インターフェースとその他の型

インターフェースとその他の型

インターフェース

Goのインターフェースは、オブジェクトの動作を指定する方法を提供します:何かがこれをできるなら、ここで使用できます。いくつかの簡単な例をすでに見ました。カスタムプリンターはStringメソッドによって実装でき、FprintfWriteメソッドを持つものなら何にでも出力を生成できます。1つまたは2つのメソッドのみを持つインターフェースはGoコードでは一般的で、通常はメソッドから派生した名前が付けられます。Writeを実装するものにはio.Writerのように。

型は複数のインターフェースを実装できます。たとえば、コレクションは、Len()Less(i, j int) boolSwap(i, j int)を含むsort.Interfaceを実装していれば、パッケージsortのルーチンでソートでき、カスタムフォーマッターも持つことができます。この作られた例では、Sequenceは両方を満たします。

type Sequence []int

// sort.Interfaceに必要なメソッド
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Copyはシーケンスのコピーを返します
func (s Sequence) Copy() Sequence {
    copy := make(Sequence, 0, len(s))
    return append(copy, s...)
}

// 印刷用メソッド - 印刷前に要素をソートします
func (s Sequence) String() string {
    s = s.Copy() // コピーを作成し、引数を上書きしません
    sort.Sort(s)
    str := "["
    for i, elem := range s { // ループはO(N²)ですが、次の例で修正します
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

変換

SequenceStringメソッドは、Sprintがスライスに対してすでに行う作業を再作成しています。(また、O(N²)の複雑さを持ち、これは不十分です。)Sprintを呼び出す前にSequenceを普通の[]intに変換すれば、努力を共有(そして高速化)できます。

func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

このメソッドは、StringメソッドからSprintfを安全に呼び出すための変換テクニックのもう1つの例です。型名を無視すれば、2つの型(Sequence[]int)は同じであるため、それらの間の変換は合法です。変換は新しい値を作成せず、既存の値が新しい型を持つかのように一時的に動作するだけです。(整数から浮動小数点への変換のように、新しい値を作成する他の合法な変換もあります。)

異なるメソッドセットにアクセスするために式の型を変換することは、Goプログラムの慣用句です。例として、既存の型sort.IntSliceを使用して、例全体を次のように減らすことができます:

type Sequence []int

// 印刷用メソッド - 印刷前に要素をソートします
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

今度は、Sequenceに複数のインターフェース(ソートと印刷)を実装させる代わりに、データ項目を複数の型(Sequencesort.IntSlice[]int)に変換する能力を使用し、それぞれがジョブの一部を実行します。実際にはより珍しいですが、効果的である場合があります。

インターフェース変換と型アサーション

型スイッチは変換の一形式です:インターフェースを取り、スイッチの各ケースに対して、ある意味でそのケースの型に変換します。これは、fmt.Printfの下のコードが型スイッチを使用して値を文字列に変換する方法の簡略化されたバージョンです。既に文字列の場合、インターフェースが保持する実際の文字列値が必要であり、Stringメソッドを持つ場合はメソッドを呼び出した結果が必要です。

type Stringer interface {
    String() string
}

var value interface{} // 呼び出し元によって提供される値
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

最初のケースは具体的な値を見つけ、2番目はインターフェースを別のインターフェースに変換します。このように型を混在させることは完全に良いことです。

我々が気にする型が1つだけの場合はどうでしょうか?値がstringを保持していることがわかっていて、それを抽出したいだけの場合は?1ケースの型スイッチでも動作しますが、型アサーションでも動作します。型アサーションはインターフェース値を取り、そこから指定された明示的な型の値を抽出します。構文は型スイッチを開く句から借用していますが、typeキーワードではなく明示的な型を使用します:

value.(typeName)

結果は、静的型typeNameを持つ新しい値です。その型は、インターフェースが保持する具体的な型であるか、値が変換できる2番目のインターフェース型である必要があります。値に含まれている文字列を抽出するために、次のように書くことができます:

str := value.(string)

しかし、値に文字列が含まれていない場合、プログラムは実行時エラーでクラッシュします。それを防ぐために、「comma, ok」慣用句を使用して、安全に値が文字列かどうかをテストします:

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

型アサーションが失敗すると、strは存在し、文字列型になりますが、ゼロ値、空の文字列を持ちます。

機能の例として、このセクションを開いた型スイッチと等価なif-elseステートメントがあります。

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

一般性

型がインターフェースを実装するためだけに存在し、そのインターフェースを超えてエクスポートされたメソッドを持つことがない場合、型自体をエクスポートする必要はありません。インターフェースのみをエクスポートすることで、値がインターフェースで記述されるもの以外の興味深い動作を持たないことが明確になります。また、共通メソッドのすべてのインスタンスでドキュメントを繰り返す必要もなくなります。

そのような場合、コンストラクタは実装型ではなくインターフェース値を返すべきです。例として、ハッシュライブラリでは、crc32.NewIEEEadler32.Newの両方がインターフェース型hash.Hash32を返します。Goプログラムでは、CRC-32アルゴリズムをAdler-32に置き換えるにはコンストラクタ呼び出しを変更するだけで済みます。コードの残りの部分はアルゴリズムの変更に影響されません。

同様のアプローチにより、さまざまなcryptoパッケージのストリーミング暗号アルゴリズムを、それらが連鎖するブロック暗号から分離できます。crypto/cipherパッケージのBlockインターフェースは、単一のデータブロックの暗号化を提供するブロック暗号の動作を指定します。次に、bufioパッケージと類推して、このインターフェースを実装する暗号パッケージは、ブロック暗号化の詳細を知ることなく、Streamインターフェースで表されるストリーミング暗号を構築するために使用できます。

crypto/cipherインターフェースは次のようになります:

type Block interface {
    BlockSize() int
    Encrypt(dst, src []byte)
    Decrypt(dst, src []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

これは、ブロック暗号をストリーミング暗号に変換するカウンターモード(CTR)ストリームの定義です。ブロック暗号の詳細が抽象化されていることに注意してください:

// NewCTRは、指定されたBlockをカウンターモードで使用して暗号化/復号化するStreamを返します。
// ivの長さは、Blockのブロックサイズと同じである必要があります。
func NewCTR(block Block, iv []byte) Stream

NewCTRは、1つの特定の暗号化アルゴリズムとデータソースだけでなく、Blockインターフェースと任意のStreamの任意の実装に適用されます。インターフェース値を返すため、CTR暗号化を他の暗号化モードに置き換えることは局所的な変更です。コンストラクタ呼び出しを編集する必要がありますが、周囲のコードは結果をStreamとしてのみ扱う必要があるため、違いに気付きません。

インターフェースとメソッド

ほとんど何でもメソッドを添付できるため、ほとんど何でもインターフェースを満たすことができます。説明的な例の1つはhttpパッケージにあり、Handlerインターフェースを定義しています。Handlerを実装するオブジェクトはHTTPリクエストを処理できます。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter自体は、クライアントにレスポンスを返すために必要なメソッドへのアクセスを提供するインターフェースです。これらのメソッドには標準のWriteメソッドが含まれるため、http.ResponseWriterio.Writerを使用できる場所で使用できます。Requestは、クライアントからのリクエストの解析された表現を含む構造体です。

簡潔にするために、POSTを無視し、HTTPリクエストは常にGETであると仮定しましょう。この簡略化はハンドラの設定方法に影響しません。ページが訪問された回数をカウントするハンドラの簡単な実装を次に示します。

// 簡単なカウンターサーバー
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

(テーマに沿って、Fprintfhttp.ResponseWriterに印刷できることに注意してください。)実際のサーバーでは、ctr.nへのアクセスは同時アクセスからの保護が必要です。提案については、syncatomicパッケージを参照してください。

参考のために、そのようなサーバーをURLツリーのノードに添付する方法を次に示します。

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

しかし、なぜCounterを構造体にするのでしょうか?整数で十分です。(レシーバーはポインターである必要があり、インクリメントが呼び出し元に見えるようにする必要があります。)

// より簡単なカウンターサーバー
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

プログラムにページが訪問されたことを通知する必要がある内部状態がある場合はどうでしょうか?チャネルをWebページに結び付けます。

// 各訪問で通知を送信するチャネル
// (おそらくチャネルをバッファリングしたいでしょう。)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

最後に、サーバーバイナリを呼び出すときに使用される引数を/argsで表示したいとしましょう。引数を印刷する関数を書くのは簡単です。

func ArgServer() {
    fmt.Println(os.Args)
}

これをHTTPサーバーに変換するにはどうすればよいでしょうか?ArgServerを、その値を無視する何らかの型のメソッドにすることもできますが、よりクリーンな方法があります。ポインタとインターフェース以外の任意の型にメソッドを定義できるため、関数にメソッドを書くことができます。httpパッケージには次のコードが含まれています:

// HandlerFunc型は、適切なシグネチャを持つ普通の関数を
// HTTPハンドラとして使用できるようにするアダプターです。
// fが適切なシグネチャを持つ関数の場合、HandlerFunc(f)は
// fを呼び出すHandlerオブジェクトです。
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTPはf(w, req)を呼び出します。
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

HandlerFuncServeHTTPメソッドを持つ型なので、その型の値はHTTPリクエストを処理できます。メソッドの実装を見てください:レシーバーは関数fで、メソッドはfを呼び出します。これは奇妙に見えるかもしれませんが、レシーバーがチャネルでメソッドがチャネルで送信するのとそれほど変わりません。

ArgServerをHTTPサーバーにするために、まず適切なシグネチャを持つように修正します。

// 引数サーバー
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

ArgServerは今度はHandlerFuncと同じシグネチャを持つので、SequenceIntSliceに変換してIntSlice.Sortにアクセスしたのと同じように、そのメソッドにアクセスするためにその型に変換できます。設定コードは簡潔です:

http.Handle("/args", http.HandlerFunc(ArgServer))

誰かがページ/argsを訪問すると、そのページにインストールされたハンドラは値ArgServerと型HandlerFuncを持ちます。HTTPサーバーはその型のServeHTTPメソッドを呼び出し、ArgServerをレシーバーとして、それは順番にArgServerを呼び出します(HandlerFunc.ServeHTTP内の呼び出しf(w, req)を介して)。引数が表示されます。

このセクションでは、構造体、整数、チャネル、関数からHTTPサーバーを作成しました。これらすべてが可能なのは、インターフェースが(ほとんど)あらゆる型に定義できるメソッドのセットにすぎないからです。