実践Go言語(Effective Go)の翻訳、10回目です。
前回までの訳は実践Go言語[日本語訳]にまとめてあります。
インタフェースとそれ以外の型
インタフェース
Go言語のインタフェースは、オブジェクトの振舞いを規定する手立てです。このセクションではインタフェースにより実現できることすべてを説明します。今まですでに2、3の簡単な例を見てきました。たとえばカスタム出力はString
メソッドを実装することで作られ、一方Fprintf
はWrite
メソッドによって出力先をどこへでも変更できます。Go言語のコードでは、通常インタフェースは1~2個のメソッドしか持たず、また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] } // 出力用メソッド - 出力前に要素を並び替え func (s Sequence) String() string { sort.Sort(s) str := "[" for i, elem := range s { if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
変換
さきほどのSequence
のString
メソッドを変更して、Sprint
が本来持っているスライス出力機能を利用するようにしました。Sprint
を呼び出す前にSequence
を純粋な[]int
に変換することで、既存の処理を利用することが可能になります。
func (s Sequence) String() string { sort.Sort(s) return fmt.Sprint([]int(s)) }
このとき変換が行われ、s
がただのスライスとみなされるため、スライスに規定されている書式で文字列が返されます。ここで変換を行われなければ、Sprint
はSequence
のString
メソッドを見つけ出し、呼び出しを無限に繰り返してしまいます。この2つの型(Sequence
と[]int
)は名前を除けば同一であるため、これらの型の間における変換は問題なく行われます。この変換では新しい値が作られることはなく、既存の値が一時的に新しい型を持つかのような働きをします。(これと異なる変換もあります。たとえば整数を浮動小数点へ変換したときは新しく値が作成されます。)
Go言語のプログラムでは、異なるメソッド群を使用するために式の型を変換することがよく行われます。例として、既存の型sort.IntArray
を使ってサンプルプログラム全体を下のように軽量化することが可能です。
type Sequence []int // 出力用メソッド - 出力する前に要素を並び替え func (s Sequence) String() string { sort.IntArray(s).Sort() return fmt.Sprint([]int(s)) }
これで、Sequence
に複数のインタフェース(ソートと出力)を実装する代わりに、データを複数の型(Sequence
、 sort.IntArray
、[]int
)に変換できることを利用して各機能を実現できるようになりました。このような使い方をすることは実際にはほとんどありませんが効果的な使い方です。
概説
ある型がインタフェースをひとつだけ実装していて、そのインタフェースのメソッド以外にエクスポートされたメソッドを持たないならば、型自体のエクスポートは不要です。インタフェースだけをエクスポートすることで、次の点を明白にすることができます。ひとつは、重要なのは実装ではなく振舞いであること。もうひとつは、振舞いは同じでも異なる機能を持った別個の実装であることです。また、共通メソッドを実装している各箇所で、その都度ドキュメントを記述する手間が省けるという利点もあります。
このような場合は、コンストラクタは実装している型ではなく、インタフェース値を返さなければなりません。一例として、ハッシュライブラリのcrc32.NewIEEE()
とadler32.New()
では、双方ともhash.Hash32
インタフェース型の値を返します。Go言語プログラムでこのハッシュアルゴリズムをAdler-32からCRC-32に変更するために必要なのは、コンストラクタの呼び出しを入れ替えるだけです。それ以外のコードは、ハッシュアルゴリズムの変更による影響を受けません。
同様のアプローチにより、crypto/block
パッケージのストリーム暗号アルゴリズムは、一連のブロック暗号から独立しています。これらは特定の実装を返すのではなく、bufio
パッケージにならってCipher
インタフェースをラップしたhash.Hash
、io.Reader
、io.Writer
いずれかのインタフェース値を返します。
下はcrypto/block
内のインタフェースです。
type Cipher interface { BlockSize() int Encrypt(src, dst []byte) Decrypt(src, dst []byte) } // NewECBDecrypterは、rからデータを読み込み、 // c内の電子コードブック(ECB)モードを使って復号化を行うReaderを返します。 func NewECBDecrypter(c Cipher, r io.Reader) io.Reader // NewCBCDecrypterは、rからデータを読み込み、 // c内の暗号ブロックチェイニング(CBC)モードと初期化ベクタivを使って // 復号化を行うReaderを返します。 func NewCBCDecrypter(c Cipher, iv []byte, r io.Reader) io.Reader
NewECBDecrypter
およびNewCBCReader
で扱うことができるのは、特定の暗号化アルゴリズムやデータソースではなく、どのCipher
インタフェースの実装であっても、またどのio.Reader
でも適用することが可能です。これらはio.Reader
インタフェース値を返すため、ECB暗号化をCBC暗号化と入れ替えるときは部分的な変更だけですみます。コンストラクタを呼び出している箇所は変更が必要ですが、その周囲のコードではコンストラクタから返されるのはio.Reader
だけとみなしていれば違いに気づくことさえないでしょう。
インタフェースとメソッド
ほぼすべての型に対してメソッドを付け加えることができるので、これはすなわち、どのインタフェースであっても、ほとんどの型に実装可能であると言えます。この実例のひとつが、Handler
インタフェースを定めているhttp
パッケージにあります。Handler
を実装しているすべてのオブジェクトで、HTTPリクエストを処理することが可能です。
type Handler interface { ServeHTTP(*Conn, *Request) }
ここでは簡略化のため、POSTは無視してHTTPリクエストが常にGETであると仮定します。 (この簡略化がハンドラの書き方に影響を及ぼすことはありません。) 下は、ページの訪問回数を単に数えるだけのハンドラの実装一式です。
// 単純なカウントサーバ type Counter struct { n int } func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) { ctr.n++ fmt.Fprintf(c, "counter = %d\n", ctr.n) }
(今回のテーマを気にとめつつ、Fprintf
がどのようにしてHTTP接続を出力するか注意してみてください。)
参考までに、こういったサーバをURLパスに割り当てる手順です。
import "http" ... ctr := new(Counter) http.Handle("/counter", ctr)
ところで、なぜCounter
を構造体としているのでしょうか。必要なのは整数ひとつだけのはずです。(ただし値が増えたことを呼び出し側にも伝わるように、レシーバはポインタである必要があります。)
// 単純なカウントサーバ type Counter int func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) { *ctr++ fmt.Fprintf(c, "counter = %d\n", *ctr) }
自作プログラムが内部ステータスを持っていて、そこにページが訪問されたことを通知しなければならないとしたらどうすればよいでしょうか。このようなときは、次のようにチャネルとウェブページとを関連付けてください。
// 訪問がある度に通知を送信するチャネル // (たぶんバッファリングされている必要がある) type Chan chan *http.Request func (ch Chan) ServeHTTP(c *http.Conn, req *http.Request) { ch <- req fmt.Fprint(c, "notification sent") }
最後に、パス/args
を訪問したときにサーバプログラムを起動した際に与えられた引数を出力するようにしてみましょう。引数の出力関数は下のように簡単に書けます。
func ArgServer() { for i, s := range os.Args { fmt.Println(s) } }
これをHTTPサーバへ変更していきます。さきほどのArgServer
関数を適当な型(値は何でも良い)のメソッドとすることもできますが、それより良い方法があります。メソッドはポインタとインタフェース以外すべての型に定義することができるため、関数に対してメソッドを書くことも可能です。http
パッケージには、下のコードが含まれています。
// HandlerFunc型は、通常の関数をHTTPハンドラとして // 使用可能にするためのアダプタです。 // fが適切なシグネチャを持つ関数であれば、HandlerFunc(f)は // fを呼び出すハンドラオブジェクトとなります。 type HandlerFunc func(*Conn, *Request) // ServeHTTPはf(c, req)を呼び出す func (f HandlerFunc) ServeHTTP(c *Conn, req *Request) { f(c, req) }
HandlerFunc
はServeHTTP
メソッドを持つ型であるため、この型の値はHTTPリクエストを処理できます。メソッドの実装を見てください。このメソッドのレシーバは関数f
であり、メソッド内でf
を呼び出しています。これは少々風変わりではありますが、前出のチャネルをレシーバとしてそのチャネルへ送信するメソッドと大差ありません。
ArgServer
をHTTPサーバにするため、まずは正しいシグネチャを持つように修正します。
// 引数サーバ func ArgServer(c *http.Conn, req *http.Request) { for i, s := range os.Args { fmt.Fprintln(c, s) } }
これでArgServer
はHandlerFunc
と同じシグネチャを持つようになったので、以前Sequence
をIntArray.Sort
を使用するためIntArray
に変換したときと同様に、ArgServer
をHandlerFunc
内のメソッドを使用するためにHandlerFunc
型に変換可能になりました。このセットアップを行うコードは次のように簡潔に書けます。
http.Handle("/args", http.HandlerFunc(ArgServer))
誰かが/args
ページを訪問したときに呼び出されるハンドラは、値はArgServer
で型はHandlerFunc
となりました。まず、HTTPサーバによってArgServer
をレシーバとしてHandlerFunc
型のServeHTTP
メソッドが実行され、続いてHandlerFunc.ServeHTTP
内のf(c, req)
を通してArgServer
が呼び出されます。そのあと引数の表示が行われます。
このセクションで、構造体、整数、チャネル、関数を使ってHTTPサーバを作成したのは、インタフェースがまさにメソッド群であり、(ほとんど)すべての型に対して定義できることを示すためです。
Comments