実践Go言語(Effective Go)の翻訳、10回目です。
前回までの訳は実践Go言語[日本語訳]にまとめてあります。


インタフェースとそれ以外の型

インタフェース

Go言語のインタフェースは、オブジェクトの振舞いを規定する手立てです。このセクションではインタフェースにより実現できることすべてを説明します。今まですでに2、3の簡単な例を見てきました。たとえばカスタム出力はStringメソッドを実装することで作られ、一方FprintfWriteメソッドによって出力先をどこへでも変更できます。Go言語のコードでは、通常インタフェースは1~2個のメソッドしか持たず、また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]
}

// 出力用メソッド - 出力前に要素を並び替え
func (s Sequence) String() string {
    sort.Sort(s)
    str := "["
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

変換

さきほどのSequenceStringメソッドを変更して、Sprintが本来持っているスライス出力機能を利用するようにしました。Sprintを呼び出す前にSequenceを純粋な[]intに変換することで、既存の処理を利用することが可能になります。

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

このとき変換が行われ、sがただのスライスとみなされるため、スライスに規定されている書式で文字列が返されます。ここで変換を行われなければ、SprintSequenceStringメソッドを見つけ出し、呼び出しを無限に繰り返してしまいます。この2つの型(Sequence[]int)は名前を除けば同一であるため、これらの型の間における変換は問題なく行われます。この変換では新しい値が作られることはなく、既存の値が一時的に新しい型を持つかのような働きをします。(これと異なる変換もあります。たとえば整数を浮動小数点へ変換したときは新しく値が作成されます。)

Go言語のプログラムでは、異なるメソッド群を使用するために式の型を変換することがよく行われます。例として、既存の型sort.IntArrayを使ってサンプルプログラム全体を下のように軽量化することが可能です。

type Sequence []int

// 出力用メソッド - 出力する前に要素を並び替え
func (s Sequence) String() string {
    sort.IntArray(s).Sort()
    return fmt.Sprint([]int(s))
}

これで、Sequenceに複数のインタフェース(ソートと出力)を実装する代わりに、データを複数の型(Sequencesort.IntArray[]int)に変換できることを利用して各機能を実現できるようになりました。このような使い方をすることは実際にはほとんどありませんが効果的な使い方です。

概説

ある型がインタフェースをひとつだけ実装していて、そのインタフェースのメソッド以外にエクスポートされたメソッドを持たないならば、型自体のエクスポートは不要です。インタフェースだけをエクスポートすることで、次の点を明白にすることができます。ひとつは、重要なのは実装ではなく振舞いであること。もうひとつは、振舞いは同じでも異なる機能を持った別個の実装であることです。また、共通メソッドを実装している各箇所で、その都度ドキュメントを記述する手間が省けるという利点もあります。

このような場合は、コンストラクタは実装している型ではなく、インタフェース値を返さなければなりません。一例として、ハッシュライブラリのcrc32.NewIEEE()adler32.New()では、双方ともhash.Hash32インタフェース型の値を返します。Go言語プログラムでこのハッシュアルゴリズムをAdler-32からCRC-32に変更するために必要なのは、コンストラクタの呼び出しを入れ替えるだけです。それ以外のコードは、ハッシュアルゴリズムの変更による影響を受けません。

同様のアプローチにより、crypto/blockパッケージのストリーム暗号アルゴリズムは、一連のブロック暗号から独立しています。これらは特定の実装を返すのではなく、bufioパッケージにならってCipherインタフェースをラップしたhash.Hashio.Readerio.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)
}

HandlerFuncServeHTTPメソッドを持つ型であるため、この型の値はHTTPリクエストを処理できます。メソッドの実装を見てください。このメソッドのレシーバは関数fであり、メソッド内でfを呼び出しています。これは少々風変わりではありますが、前出のチャネルをレシーバとしてそのチャネルへ送信するメソッドと大差ありません。

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

// 引数サーバ
func ArgServer(c *http.Conn, req *http.Request) {
    for i, s := range os.Args {
        fmt.Fprintln(c, s)
    }
}

これでArgServerHandlerFuncと同じシグネチャを持つようになったので、以前SequenceIntArray.Sortを使用するためIntArrayに変換したときと同様に、ArgServerHandlerFunc内のメソッドを使用するためにHandlerFunc型に変換可能になりました。このセットアップを行うコードは次のように簡潔に書けます。

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

誰かが/argsページを訪問したときに呼び出されるハンドラは、値はArgServerで型はHandlerFuncとなりました。まず、HTTPサーバによってArgServerをレシーバとしてHandlerFunc型のServeHTTPメソッドが実行され、続いてHandlerFunc.ServeHTTP内のf(c, req)を通してArgServerが呼び出されます。そのあと引数の表示が行われます。

このセクションで、構造体、整数、チャネル、関数を使ってHTTPサーバを作成したのは、インタフェースがまさにメソッド群であり、(ほとんど)すべての型に対して定義できることを示すためです。