Golang Cafe #19 まとめ 「Twelve Go Best Practices」を読む(ソースコード説明編)

2014/03/02に開催された「Golang Cafe #19」についてのまとめです。

今回から何回かに分けてTwelve Go Best Practicesを読んでいきます。
ソースコードこちらにあります。historyを追っていただければ下記で説明する手順を確認できます。

1. Avoid nesting by handling errors first(最初にエラー処理を行うことでネストを避ける)

説明をするために簡単なコードです。

type Gopher struct {
    Name     string
    AgeYears int
}

func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
    err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err == nil {
        size += 4
        var n int
        n, err = w.Write([]byte(g.Name))
        size += int64(n)
        if err == nil {
            err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
            if err == nil {
                size += 4
            }
            return
        }
        return
    }
    return
}

Gopher構造体をWriterに書き出すメソッドです。
err == nilのチェックを行うたびにネストが深くなっていき、非常に見難くなっています。
このコードをタイトルにもあるように、最初にエラー処理(err != nilの場合の処理)を行うことで深いネストを解消していきます。

func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
    err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err != nil {
        return
    }
    size += 4
    n, err := w.Write([]byte(g.Name))
    size += int64(n)
    if err != nil {
        return
    }
    err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
    if err == nil {
        size += 4
    }
    return
}

不要なネストがなくなり見通しが良くなりました。

2. Avoid repetition when possible(可能な場合重複を避ける)

先の修正でネストは浅くなりコードの見通しも大変良くなりました。
ただ、よく見ると、

  1. Writeする
  2. エラーチェック(err != nilの場合はreturn)
  3. サイズを加算する

という同じ処理が繰り返されています。今度はこの繰り返しを取り除く方法を考えてみます。

type binWriter struct {
    w    io.Writer
    size int64
    err  error
}

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
        w.size += int64(binary.Size(v))
    }
}

binWriter構造体とWriteメソッドを定義します。構造体はWriterと書き込んだサイズ、および発生したerrorを保持しています。またWriteメソッドでは最初にエラーチェックを行ない、以前にWriteメソッドが呼ばれた際にエラーが発生していた場合は、実際にWriteは行わず、即メソッドを抜けるようになっています。
こうすることで不要なWrite処理を行わないようにしています。
呼び出し元のメソッドは以下のように修正されます。

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(int32(len(g.Name)))
    bw.Write([]byte(g.Name))
    bw.Write(int64(g.AgeYears))
    return bw.size, bw.err
}

binWriter内でエラーチェックおよび処理を行っているのでとてもシンプルになりました。
先にも書いたとおり、エラーかどうか確認せずにbw.Writeメソッドをよんでいますが、bw.Writeメソッドが以前のエラー有無を確認して実際の書き込み処理を行っています。最後に書き込んだサイズとエラーを返しています。

Type switch to handle special cases(特定のケースを処理するために型switchを使用します)

先の例では構造体のフィールドの型によってbw.Writeメソッドを呼び出す回数が異なります。(Gopher.Nameだと2回、Gopher.AgeYearsだと1回)
これをどのフィールドでも1回のbw.Writeメソッドを呼び出すだけでよいように修正します。

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch v.(type) {
    case string:
        s := v.(string)
        w.Write(int32(len(s)))
        w.Write([]byte(s))
    case int:
        i := v.(int)
        w.Write(int64(i))
    default:
        if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
            w.size += int64(binary.Size(v))
        }
    }
}

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.AgeYears)
    return bw.size, bw.err
}

型switchを使用して、引数に指定された値の型ごとに処理を分けます。stringの場合は長さとbyte配列を書き出します。整数の場合は値のみを書き出します。(実際には自身のメソッドを再帰呼び出ししています)
こうすることでどのフィールドに対してもbw.Writeメソッドを1回呼ぶだけでよくなりました。

Type switch with short variable declaration(ローカルスコープ変数宣言を使用する型switch)

先の例ではcase文の中で都度TypeAssertionを使用して型を変換してきました。しかしGoでは型switchで変数宣言することで、case文ではその変数の型が使用され、宣言した変数は該当する型にキャストされた値としてswitchブロック内で使用することができます。

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch x := v.(type) {
    case string:
        w.Write(int32(len(x)))
        w.Write([]byte(x))
    case int:
        w.Write(int64(x))
    default:
        if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
            w.size += int64(binary.Size(v))
        }
    }
}

case文内で行っていたTypeAssertionを削除することができました。

Writing everything or nothing(すべて書き込むかすべて書き込まないか)

先の例ではbinWriter.Writeメソッドの先頭で以前にエラーが発生していたが書き込み処理を行わないような実装になっています。しかし、仮に100回の書き込み処理が必要で、99回目まで成功し、100回目でエラーが発生した場合、99回の書き込みが無駄になってしまいます。
そこですべての書き込みを一度に行うように修正します。

type binWriter struct {
    w   io.Writer
    buf bytes.Buffer
    err error
}

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch x := v.(type) {
    case string:
        w.Write(int32(len(x)))
        w.Write([]byte(x))
    case int:
        w.Write(int64(x))
    default:
        w.err = binary.Write(&w.buf, binary.LittleEndian, v)
    }
}

func (w *binWriter) Flush() (int64, error) {
    if w.err != nil {
        return 0, w.err
    }
    return w.buf.WriteTo(w.w)
}

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.AgeYears)
    return bw.Flush()
}

binWriterにバッファのフィールドを用意し、binWriter.Writeメソッドで実際に書き出していた処理をバッファに書きだすように修正します。新たにbinWriter.Flushメソッドを用意し、このメソッドが呼ばれた時点で実際に書きだすようにします。ただ書き出すデータ量が多い場合はメモリを圧迫するので考慮が必要です。

Function adapters(関数アダプタ)

以下の様なコードがあったとします。

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    err := doThis()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("handling %q: %v", r.RequestURI, err)
        return
    }

    err = doThat()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("handling %q: %v", r.RequestURI, err)
        return
    }
}

この例では同じ「エラー処理」を繰り返しています。この同じエラー処理をどう纏めるかを考えます。

func init() {
    http.HandleFunc("/", errorHandler(betterHandler))
}

func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        err := f(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            log.Printf("handling %q: %v", r.RequestURI, err)
        }
    }
}

func betterHandler(w http.ResponseWriter, r *http.Request) error {
    if err := doThis(); err != nil {
        return fmt.Errorf("doing this: %v", err)
    }

    if err := doThat(); err != nil {
        return fmt.Errorf("doing that: %v", err)
    }
    return nil
}

共通するエラー処理を行う関数(errorHandler)を作りエラー処理を定義しておきます。そして特定の処理を定義した関数をエラー処理を行う関数に渡し、その中で実行してもらいます。
Javaでいうところの無名クラス、C#でいうとデリゲート、Cなどでいうと関数ポインタのような使い方でしょうか。
エラー処理に限ったことではなく、「複数の関数があり、ほとんど同じ処理を行うが、各関数で同じ一部だけ処理が異なる」といった場合にも適用できます。


先日私が公開した、go-pop3でも使用しています。pop3.goの109、164、321行目あたりがこれにあたります。