インターフェースとその他の型
インターフェース
Goのインターフェースは、オブジェクトの動作を指定する方法を提供します:何かがこれをできるなら、ここで使用できます。いくつかの簡単な例をすでに見ました。カスタムプリンターはString
メソッドによって実装でき、Fprintf
はWrite
メソッドを持つものなら何にでも出力を生成できます。1つまたは2つのメソッドのみを持つインターフェースはGoコードでは一般的で、通常はメソッドから派生した名前が付けられます。Write
を実装するものにはio.Writer
のように。
型は複数のインターフェースを実装できます。たとえば、コレクションは、Len()
、Less(i, j int) bool
、Swap(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 + "]"
}
変換
Sequence
のString
メソッドは、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
に複数のインターフェース(ソートと印刷)を実装させる代わりに、データ項目を複数の型(Sequence
、sort.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.NewIEEE
とadler32.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.ResponseWriter
はio.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)
}
(テーマに沿って、Fprintf
がhttp.ResponseWriter
に印刷できることに注意してください。)実際のサーバーでは、ctr.n
へのアクセスは同時アクセスからの保護が必要です。提案については、sync
とatomic
パッケージを参照してください。
参考のために、そのようなサーバーを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)
}
HandlerFunc
はServeHTTP
メソッドを持つ型なので、その型の値はHTTPリクエストを処理できます。メソッドの実装を見てください:レシーバーは関数f
で、メソッドはf
を呼び出します。これは奇妙に見えるかもしれませんが、レシーバーがチャネルでメソッドがチャネルで送信するのとそれほど変わりません。
ArgServer
をHTTPサーバーにするために、まず適切なシグネチャを持つように修正します。
// 引数サーバー
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
ArgServer
は今度はHandlerFunc
と同じシグネチャを持つので、Sequence
をIntSlice
に変換してIntSlice.Sort
にアクセスしたのと同じように、そのメソッドにアクセスするためにその型に変換できます。設定コードは簡潔です:
http.Handle("/args", http.HandlerFunc(ArgServer))
誰かがページ/args
を訪問すると、そのページにインストールされたハンドラは値ArgServer
と型HandlerFunc
を持ちます。HTTPサーバーはその型のServeHTTP
メソッドを呼び出し、ArgServer
をレシーバーとして、それは順番にArgServer
を呼び出します(HandlerFunc.ServeHTTP
内の呼び出しf(w, req)
を介して)。引数が表示されます。
このセクションでは、構造体、整数、チャネル、関数からHTTPサーバーを作成しました。これらすべてが可能なのは、インターフェースが(ほとんど)あらゆる型に定義できるメソッドのセットにすぎないからです。