Effective Go 野良翻訳(3)

続き。これで3/4かな。初期化、メソッド、インターフェイス、埋め込み。

初期化

Goの初期化は、CやC++の初期化とそれほど違わないようにみえるが、よりパワフルである。複雑な構造体も初期化で作ることができるし、複数パッケージ間のオブジェクトの初期化順序の問題もきちんと扱われている。

定数

Goの定数は、単なる、、、定数だ。定数はコンパイル時に生成される。関数内部のローカル変数である場合にもだ。定数は、数値、文字列、真偽値のいずれかだ。コンパイル時に生成されるという制約上、定数を定義する式はコンパイラで評価される定数式でなければならない。たとえば、1<<3は定数式だが、math.Sin(math.Pi/4)はmath.Sinがプログラム実行時に実行されるので定数式ではない。

Goでは、列挙型の定数はiotaという列挙子(enumeratr)を用いて生成する。iotaは式の一部として利用でき,暗黙裏に反復されるので、複雑な値の列を簡単に作ることができる。

type ByteSize float64
const (
    _ = iota  // 最初の値はブランク識別しに代入して無視
    KB ByteSize = 1<<(10*iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

型に対してもメソッドを定義できる。たとえばStringメソッドを定義しておけば、一般的な型の一部として出力することができるようになる。

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", float64(b/YB))
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", float64(b/ZB))
    case b >= EB:
        return fmt.Sprintf("%.2fEB", float64(b/EB))
    case b >= PB:
        return fmt.Sprintf("%.2fPB", float64(b/PB))
    case b >= TB:
        return fmt.Sprintf("%.2fTB", float64(b/TB))
    case b >= GB:
        return fmt.Sprintf("%.2fGB", float64(b/GB))
    case b >= MB:
        return fmt.Sprintf("%.2fMB", float64(b/MB))
    case b >= KB:
        return fmt.Sprintf("%.2fKB", float64(b/KB))
    }
    return fmt.Sprintf("%.2fB", float64(b))
}

(float64に型変換することで、Sprintfが再びByteSizeのStringメソッドを呼び出すことを防いでいる)。YBは1.00YBと出力され、ByteSize(1e13)は9.09TBと出力される。

変数

変数は、定数と同じように初期化できるが、初期化式は実行時に実行される一般的な式でかまわない。

var (
    HOME = os.Getenv("HOME")
    USER = os.Getenv("USER")
    GOROOT = os.Getenv("GOROOT")
)
初期化関数

最後に、個々のソースファイルに、必要な設定を行うinit()関数を定義することができる。唯一の制約は、初期化の最中にgoroutineを起動することはできるが、実際にそのgoroutineが走り始めるのは初期化が終了してからだ、ということだ。初期化は、常に単一スレッドで行われる。「最後に」と書いたがinit()関数は本当に最後に呼ばれる。init()関数は、インポートしているパッケージの初期化がすべて終わり、さらに、そのパッケージに含まれる変数宣言が初期化を終了した後で実行されるのだ。

init()関数は、宣言時に表現できないような初期化を行う以外に、実際に実行が開始される前にプログラムの状態を補正するためにも用いられる。

func init() {
    if USER == "" {
        log.Exit("$USER not set")
    }
    if HOME == "" {
        HOME = "/usr/" + USER
    }
    if GOROOT == "" {
        GOROOT = HOME + "/go"
    }
    // GOROOT may be overridden by --goroot flag on command line.
    flag.StringVar(&GOROOT, "goroot", GOROOT, "Go root directory")
}

メソッド

ポインタ vs. 値

メソッドはポインタとインターフェイス以外の名前のついた型すべてに定義することができる。レシーバは構造体である必要はない。

上のスライスの説明でAppend関数を示した。あの関数をスライスのメソッドとして定義することもできる。そのためには、まずメソッドの対象となる名前付き型を宣言し、そのメソッドのレシーバとして利用したいデータをその型に対応させる。

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // ボディは上での定義と全く同じ
}

この場合は、やはり更新されたスライスをリターンする必要がある。これは面倒なので、メソッドをByteSliceのポインタをとり、呼び出したスライスをオーバーライトするように再定義しよう。

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // ボディは上での定義と同じだが、returnはなし
    *p = slice
}

もっと改良することもできる。メソッドを標準のWriteメソッドと同じシグネチャにすれば、*ByteSlice型は、標準のio.Writerインターフェイスを満たすことになる。

func (p *ByteSlice) Write(data []byte) (n int, err os.Error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

こうしておくと便利だ。たとえば、*ByteSliceに対してプリントできるようになる。

    var b ByteSlice
    fmt.Fprintf(&b, "This hour has %d days\n", 7)

ここで、ByteSliceのアドレスを渡しているが、これは *ByteSlice だけがio.Writerを満たしているからだ。レシーバとしてポインタを使うべきか、値を使うべきか、は次のルールで決める。値に対するメソッドは、ポインタに対しても値に対しても起動できるが、ポインタに対するメソッドは、ポインタに対してしか起動できない。これは、ポインタメソッドはレシーバを変更する可能性があるからだ。コピーした値に対してポインタメソッドが変更しても、変更が無効になってしまうのだ。

ところで、このbyteのスライスに対してWriteするというアイディアはbytes.Bufferとして実装されている。

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

インターフェイス

Goのインターフェースは、あるモノの動作を特定するための方法だ。あることをできるなら、ここでつかえる、ということだ。Writeメソッドを持つモノはFprintfで出力できるので、型に固有のWriteメソッドを定義するなど、これまでにも簡単な例を見てきた。Goのプログラムでは、1つか2つしかメソッドを持たないインターフェイスが一般的で、その場合メソッドの名前に基づいたインターフェイス名になる。たとえば、Writeメソッドを持つモノに対するio.Writerなどがそうだ。

一つの型で複数のインターフェイスを実装することも可能だ。例を挙げよう。sort.Interface を実装した集合であれば、sortパッケージのルーチンでソートすることができる。sort.Interface は Len), Less(i, j int) bool, Swap(i, j int) メソッドを持つ。同時に固有のプリントフォーマッタを持っていてもよい。下に示す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がスライスに対して行うことをわざわざやり直している。Sequnceをただの []intに変換してからSprintに渡すようにすれば、この手間が省ける。

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

この変換によって、sは通常のスライスとして扱われるようになり、デフォルトのフォーマッティングが適用されるようになる。この変換を行わないと、Sprintが再度SequenceのStringを呼び出すことになるので無限ループになる。Sequenceと[]intの二つの型は型の名前を除いてはまったく同じなので、これらの間では問題なく変換できるのだ。変換によって値が新しく作られる訳ではない。既存の値が別の型であるかのように一時的にふるまうだけだ(整数から浮動小数点を作るような種類の変換もあるが、この場合は新しい値が作られる)。

式の型を変換して別のメソッド集合にアクセスできるようにするのは、Goではよくある書き方だ。たとえば、既存のsort.IntArrayを用いて、上の例全体を次のように書き直すことができる。

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    sort.IntArray(s).Sort()
    return fmt.Sprint([]int(s))
}

Sequenceで複数のインターフェイス(sortingとprinting)を実装するかわりに、値が複数の型(Sequence, sort.IntArray, []int)に変換できることを利用している。複数の型がそれぞれ仕事の一端を担っているわけだ。このような書き方はあまりみられないが、効率的だ。

汎化性

ある型が、インターフェイスを実装するためだけに存在し、そのインターフェイスのメソッド以外に公開メソッドをもたない場合には、その型を公開する必要はない。インターフェイスだけを公開すると、実装ではなく、重要な動作だけが明確になり、異なる属性を持つ独立した実装が、オリジナルの型の挙動をコピーすることが容易になる。また、同じようなメソッドに対してなんどもドキュメントを書く手間も省ける。
そのような場合には、コンストラクタで、実装している型の値でなくインターフェイスの値を返すようにするべきだ。たとえば、hashライブラリには crc32.NewIEEE() と adler32.New() があるが、これらはいずれもhash.Hash32インターフェイスの値を返す。CRC-32アルゴリズムAdler-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 returns a reader that reads data
// from r and decrypts it using c in electronic codebook (ECB) mode.
func NewECBDecrypter(c Cipher, r io.Reader) io.Reader

// NewCBCDecrypter returns a reader that reads data
// from r and decrypts it using c in cipher block chaining (CBC) mode
// with the initialization vector iv.
func NewCBCDecrypter(c Cipher, iv []byte, r io.Reader) io.Reader

NewECBDecrypterとNewCBCReaderは、特定の暗号アルゴリズム、データソースに依存せず、任意のCipherインターフェイスの実装と、任意のio.Reader インターフェイスの実装に対して使用できる。両方ともio.Readerインターフェイスの値を返すので、ECB暗号とCBC暗号を置き換える際の影響は局所化されている。コンストラクタの呼び出し部分だけは編集しなければならないが、周りのコードはコンストラクタの出力を単なるio.Readerとしてあつかっているので、置き換えによる影響はない。

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

ほとんどすべてのものにメソッドを定義することができるので、ほとんどすべてのものがインターフェイスを満たすことができる。わかりやすい例として、httpパッケージのHandlerインターフェイスがある。このHandlerインターフェイスを満たすオブジェクトであればなんでも、HTTPリクエストを処理することができる。

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

ResponseWriterもインターフェイスで、クライアントにレスポンスを返すために必要なメソッド群を提供する。これらのメソッド群には、標準的なWriteメソッドが含まれているので、io.Writerが利用できるものであれば、http.ResponseWriterが使用できる。Requset は、クライアントからのリクエストをパースしたものが納められた構造体だ。

簡単のため、POSTは無視してHTTPリクエストは常にGETであるとしよう。このように考えても、ハンドラの設定にはなんら関係ない。トリビアルではあるが完全なハンドラの実装を示す。このハンドラはページが訪問された回数を数える(Fprintfをhttp.ResponseWriterに使用できていることに注意)。

// Simple counter server.
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)
}

参考のため、このようなサーバをURLツリーのノードに割り当てる方法を示す。

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

しかし、Counterを構造体にする必要があるだろうか?欲しいものはただの整数だ(呼び出した側にインクリメントしたことが反映されるように、レシーバをポインタにする必要がある)。

// Simpler counter server.
type Counter int

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

さて、ページに訪問者があった場合に、何らかの内部状態に通知する必要がある場合にはどうしたらいいだろうか。チャンネルをページに結びつければいい。

// 訪問の通知を送るチャンネル
// (チャンネルはバッファリングした方がいいかも)
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() {
    for i, s := range os.Args {
        fmt.Println(s)
    }
}

これをどうやってHTTPサーバにしたらいいだろうか?ArgServer何らかの型のメソッドにして、その型の値は無視するという方法もあるが、もっときれいな方法がある。ポインタとインターフェイス意外にはメソッドが定義できるわけだから、関数にメソッドをつけることだってできるのだ。httpパッケージには以下のコードが含まれている

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

// ServeHTTP calls f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

HandlerFuncにはServeHTTPメソッドが定義されているので、この型の値はHTTPリクエストをしょりすることができる。メソッドの実装を見てみよう。レシーバは関数 f で、メソッドはその関数fを呼び出している。奇妙に見えるかも知れないが、たとえば、レシーバがチャンネルでメソッドがそのチャンネルにメッセージを送るのと、それほど変わらない。

ArgServerをHTTPサーバにするためには、まずシグネチャをあわせなければならない。

// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    for i, s := range os.Args {
        fmt.Fprintln(w, s)
    }
}

これで ArgServerのシグネチャがHandlerFuncと同じになったので、型変換してHandlerFuncのメソッドにアクセスさせることができる。SequenceをIntArrayに型変換してIntArray.Sortを呼び出すのと同じだ。コードは下に示すようにコンパクトだ。

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

誰かが、/args ページを訪問すると、そのページにインストールされたハンドラArgsServerが、型HandlerFuncとして呼び出される。Httpサーバは、その型のServeHTTPメソッドをレシーバをArgServerとして呼び出す。すると、HandlerFunc.ServeHTTPの f(c, req) で、ArgServer関数が呼び出される。そして、引数が表示される。

ここでは、HTTPサーバを構造体、整数、チャンネル、関数から作れることを示した。こんなことができるのもすべて、インターフェイスがメソッドの集合に過ぎず、メソッドはほとんどすべての型に定義できるおかげだ。

埋め込み

Goにはいわゆる型によるサブクラスという概念がない。しかし、構造体やインターフェイスに型を「埋め込む」ことによって、実装を「借りてくる」ことができる。

インターフェイスの埋め込みは簡単だ。io.Reader とio.Writerインターフェイスはすでに何度か使っているが、これらの定義は次のようになっている。

type Reader interface {
    Read(p []byte) (n int, err os.Error)
}

type Writer interface {
    Write(p []byte) (n int, err os.Error)
}

ioパッケージには、このほかに、これらのメソッドのいくつかを実装したオブジェクトを指すインターフェイスがいくつか定義されている。たとえば、io.ReadWriterはReadメソッドとWriteメソッドの両方を持つインターフェイスだ。io.ReadWriterを定義するのに、二つのメソッドを明示的に書いてもいいが、もっと簡単でわかりやすい方法がある。二つのインターフェイスを埋め込んで新しいのを定義するのだ。こんな風に。

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

これは、見たとおりのものを定義している。ReadWriterはReaderができることとWriterができることをするのだ。埋め込まれたインターフェイスの和集合になるのだ。(埋め込まれたインターフェイスのメソッド集合は重複があってはいけない。)インターフェイスに埋め込むことができるのはインターフェイスだけだ。

同じ基本的なアイディアを構造体にも使うことができる。こちらは、もっと使い出がある。bufioパッケージには二つの構造体がある。buio.Reader とbufio.Writerだ。これらは、当然、ioパッケージの対応するインターフェイスを実装している。bufioには、バッファリング付きリーダーライターも実装されている。これは、ReaderとWriter を一つの構造体に埋め込むことで実装されている。構造体にフィールド名を定義しないで型を並べている。

// ReadWriter はReaderとWriterへのポインタを保持
// io.ReadWriter を実装している
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

埋め込まれているのは構造体のポインタなので実際に使う際にはもちろん初期化しなければならない。このReadWriter構造体は次のように書くこともできる。

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

しかし、このように書いた場合には、ioで定義されたインターフェイスを満たすために、各フィールドのメソッドを次のような転送メソッドで実装し直さなければならない。

func (rw *ReadWriter) Read(p []byte) (n int, err os.Error) {
    return rw.reader.Read(p)
}

構造体を直接埋め込むと、このような手間が避けられる。埋め込まれた型のメソッドは何もしなくても使うことができる。これは、bufio.ReadWriterは、bufio.Readerとbufio.Writerのメソッドを持つと同時に、io.Reader, io.Writer, io.ReadWriterの3つのインターフェイスを満たすことを意味する。

この埋め込みとサブクラスは重要な点が異なっている。型の埋め込みで、内側の型のメソッドを外側の型のメソッドとして使うことができるが、そのメソッドを呼び出したときのレシーバ型は、外側ではなく内側の型になるのだ。この例で言えば、bufio.ReadWriterのReadメソッドを呼び出したときには、上で定義した転送メソッドを使った場合と全く同じことが起こる。つまりレシーバはReadWriterのreaderフィールドであり、ReadWriterそのものではない。

埋め込みは、単に便利にするために使う場合もある。この例では通常の名前付きフィールドと同時に埋め込みフィールドを使っている。

type Job struct {
    Command string
    *log.Logger
}

こうすると、Job型にたいしてLog, Logfなどの*log.Loggerメソッドを利用することができるようになる。もちろん、Loggerにフィールド名をつけてもいいのだが、そうする必要はない。Loggerを初期化しておくと、次のようにしてJobのログを取ることができる。

job.Log("starting now...")

Loggerは構造体の通常のフィールドなので、次のようにしてコンストラクタで初期化する。

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

コンポジットリテラルをつかってもいい。

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

埋め込まれた型に直接参照したい場合には、パッケージ名を除いた型名がフィールド名になる。もし、Job変数jobの *log.Logger であれば、job.Logger だ。この参照を使って、Logger のメソッドを書き直すこともできる。

func (job *Job) Logf(format string, args ...) {
    job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args))
}

埋め込み型によって、名前の衝突の可能性が生じる。しかし、解決のルールは簡単だまず、フィールド/メソッドX は、その型のより深い場所に埋め込まれたXを隠蔽する。たとえば、log.Logger に Command というフィールド/メソッドがあったとすると、Job の Commandによって見えなくなる。

次に、同じ名前が同じ埋め込みレベルにあった場合には、一般にはエラーになる。たとえば、Job構造体に、Loggerという名前のフィールド/メソッドが他にあった場合に、log.Loggerを埋め込むとエラーになる。ただし、重複した名前が、プログラムの型宣言以外の場所で使用されなければエラーにならない。この留保によって、外から持ってきて埋め込んだ型が変更された際の影響をある程度防ぐことができる。たとえば、ある埋め込まれた型のフィールドと衝突するフィールドが別の型に追加されたとしても、どちらも使われていなければ、問題にならないのだ。