Go Conference 2014 Autumnレポート

鵜飼文敏氏「Goに入ってはGoに従え」可読性のあるコードにするために~Go Conference 2014 Autumn基調講演2人目

この記事を読むのに必要な時間:およそ 10 分

シンプルに実装する

time.Durationを使う

同氏は,期間を表す値は以下のようにint型ではなく,time.Duration型を使うべきだと指摘していました。また,コマンドライン引数として期間を取る場合は,flag.Duration関数を使うことでtime.Duration型の値を取得できると述べていました。

var rpcTimeoutSecs = 30 // Thirty seconds

以下の2つの例では,確かにtime.Duration型を使用していますが,型変換は不要です。定数は型情報を持たないため,time.Secondと掛け算をするとその結果はtime.Duration型となります。

var rpcTimeout = time.Duration(30 * time.Second) // Thirty seconds
var rpcTimeout = time.Duration(30) * time.Second // Thirty seconds

同氏は型変換をなくすと,上記のコードは以下のように簡潔に書くことができると述べていました。また,30 * time.Secondで十分意味が通じるので,// Thirty secondsのような冗長なコメントは必要ないと指摘していました。

var rpcTimeout = 30 * time.Second

チャネルを使う

同氏は,今までC言語やC++でコードを書いてきた人たちが以下のようなコードを書きがちだと述べていました。

type Stream struct {
    // some fields
    isConnClosed     bool
    connClosedCond   *sync.Cond
    connClosedLocker sync.Mutex
}
func (s *Stream) Wait() error {
    s.connClosedCond.L.Lock()
    for !s.isConnClosed {
        s.connClosedCond.Wait()
    }
    s.connClosedCond.L.Unlock()
    // some code
}
func (s *Stream) Close() {
    // some code
    s.connClosedCond.L.Lock()
    s.isConnClosed = true
    s.connClosedCond.L.Unlock()
    s.connClosedCond.Broadcast()
}
func (s *Stream) IsClosed() bool {
    return s.isConnClosed
}

StreamCloseされているかどうかをisConnClosedで管理し,sync.Mutexsync.Condを使って制御しています。同氏は,このコードは決して間違ってはいないが,受信のみのチャネルを使うことでもっと簡潔に書けると指摘していました。

type Stream struct {
    // some fields
    cc chan struct{}
}
func (s *Stream) Wait() error {
    <-s.cc
    // some code
}
func (s *Stream) Close() {
    // some code
    close(s.cc)
}
func (s *Stream) IsClosed() bool {
    select {
    case <-s.cc:
        return true
    default:
        return false
    }
}

上記のチャネルを使ったコードでは,チャネルが開いているか閉じているかをStreamが開いているか閉じているかに対応させています。Closeメソッドはチャネルを閉じるだけで実現しています。そして,Waitメソッドは単にチャネルから受信すればよく,チャネルが閉じられれば直ちにゼロ値が送られてくるそうです。またIsClosedメソッドは,開いているチャネルから受信を行うと何かしらの値が送信されるまで処理がブロックされることを利用して実現されています。チャネルが開いていると,selectcase <-s.ccのケースは実行されず,defaultのケースが実行されます。一方,チャネルが閉じていると直ちにゼロ値が送られてくるため,case <-s.ccのケースが実行されます。

型がわかっている場合にreflectは使わない

同氏は,reflectパッケージは強力であり,効果的に使える場合もあるが安易に使うべきではないと主張していました。以下のコードは,いくつかのフィールドに対して似たような処理を行う必要があり,そのためにreflectを使用しているとのことでした。

type Layers struct {
    UI, Launch /* more fields */ string
}

    layers := NewLayers(s.Entries)
    v := reflect.ValueOf(*layers)
    r := v.Type()  // type Layers
    for i := 0; i < r.NumField(); i++ {
        if e := v.Field(i).String(); e != "-" {
            eid := &pb.ExperimentId{
                Layer:        proto.String(r.Field(i).Name()),
                ExperimentId: &e,
            }
            experimentIDs = append(experimentIDs, eid)
        }
    }

上記のコードの場合,reflectの対象のLayersがどのような型であるかコンパイル時にわかっているため,reflectを使う必要はないと同氏は指摘していました。そして,以下のようにSliceというLayerExperimentの対のスライスを返すメソッドを追加することで,同様の処理が書けると述べていました。同氏はプログラム自体は長くなるが,reflectを使うよりも意図がわかりやすくなると主張していました。

type LayerExperiment struct{ Layer, Experiment string }

func (t *Layers) Slice() []LayerExperiment {
    return []LayerExperiment{
        {"UI", t.UI},
        {"Launch", t.Launch},
        /* more fields */
    }
}

    layers := NewLayers(s.Entries).Slice()
    for _, l := range layers {
        if l.Experiment != "-" {
            eid := &pb.ExperimentId{
                Layer:        proto.String(l.Layer),
                ExperimentId: proto.String(l.Experiment),
            }
            experimentIDs = append(experimentIDs, eid)
        }
    }

テスト

同氏は可読性のレビューにおいて,テストがうまく書けていない・可読性に欠けていることを指摘することがよくあると述べていました。典型的なテストコードは以下のように,ある処理において期待した結果が出なかった場合に,t.Errorfメソッドで入力と期待する出力と実際の出力をメッセージとして出すものであると述べていました。このように書いておけば,どういう場合にどのような値が出るのかがわかりやすいとのことでした。

// 典型的なテストコード
if got, want := テスト対象(input), 期待値; !テスト(got, want) {
    t.Errorf("テスト対象(%v) = %v; want %v", input, got, want)
}

なお,多くの人が独自アサート機能を実装しがちですが,同氏は言語の機能を使うことをお勧めしていました。また,コメントにAPIの使い方を詳細に書くのであれば,以下のようなExampleテストを書くべきだと主張していました。Exampleテストはテストとして実行されるため,より正しいExampleとして提供することが可能とのことでした。

func ExampleWrite() {
    var buf bytes.Buffer
    var pi float64 = math.Pi
    err := binary.Write(&buf, binary.LittleEndian, pi)
    if err != nil {
        fmt.Println("binary.Write failed:", err)
    }
    fmt.Printf("% x", buf.Bytes())
    // Output: 18 2d 44 54 fb 21 09 40
}

著者プロフィール

上田拓也(うえだたくや)

KLab(株)所属。Go Conference 2014 Autumnスタッフ。

業務ではJavaScript,Luaなどを扱っている。

大学時代にGo言語に出会い,それ以来Go言語にのめり込む。

時間があると,Go言語の勉強会に参加している。

Go言語のマスコットのGopherの絵を描くのも好き。

Twitter:@tenntenn