Go Conference 2014 Autumnレポート

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

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

インターフェースの埋込み

以下のコードはscan.Writerインターフェースを埋め込むことでをColumnWriterscan.Writerインターフェースを実装していることを明示的にしようとしています。

// Column writer implements the scan.Writer interface.
type ColumnWriter struct {
    scan.Writer
    tmpDir      string
    // some other fields
}

たしかにscan.Writerを埋め込むことで,ColumnWriterscan.Writerを実装していることになります。しかし,初期化時にscan.Writerを埋め込む必要があるうえにこれではColumnWriterscan.Writerを本当に実装しているかどうかの保証にはなりません。同氏は,以下のようにscan.Writer型に代入することで,*ColumnWriterscan.Writerを実装しているかどうか,コンパイル時にチェックすることができると述べています。この方法であれば,*ColumnWriterscan.Writerインターフェースを実装していない場合には,コンパイルエラーとなります。なお,_で代入を受けているため,実際には変数代入は行われず,型チェックだけ行うことができます。

// ColumnWriter is a writer to write ...
type ColumnWriter struct {
    tmpDir string
    // some other fields
}

var _ scan.Writer = (*ColumnWriter)(nil)

なお,構造体がインターフェースの定義するメソッドを明示的に実装せず,さらに構造体にインターフェースを埋め込んで,その値を設定しなかった場合nilの場合⁠⁠,そのメソッドを呼び出すとpanicが発生します。

import "fmt"

type I interface {
    Key() string
    Value() string
}
type S struct{ I }  // SはIのメソッドをもつ
func (s S) Key() string { return "type S" }

func main() {
    var s S
    fmt.Println("key", s.Key())
    fmt.Println(s.Value())  // runtime error: invalid memory address or nil pointer deference
}

同氏は,この挙動はテストで一部のメソッドだけ実装したいときは便利だと述べていました。

コードを見やすくする

構造体のフィールドのレイアウト

以下のように,フィールドの並び順を意識せずに構造体を定義してしまうことがあると思います。しかし同氏は,この定義の仕方だとmuがどのフィールドを保護しているのかわからないと指摘していました。

type Modifier struct {
    pmod          *profile.Modifier
    cache         map[string]time.Time
    client        *client.Client
    mu            sync.RWMutex
}

そのため,フィールドリストは関連が深いものをブロックに分け,sync.Mutexはそれが保護しているフィールドのブロックの先頭に置くとわかりやすいとのことでした。

type Modifier struct {
    client        *client.Client

    mu            sync.RWMutex
    pmod          *profile.Modifier
    cache         map[string]time.Time
}

長い行は簡潔な変数名を使って短く

以下のように,関数の引数が多くなり1行が長くなってしまうことがあると思います。

package sampling

import (
    servicepb "foo/bar/service_proto"
)

type SamplingServer struct {
    // some fields
}

func (server *SamplingServer) SampleMetrics(
    sampleRequest *servicepb.Request, sampleResponse *servicepb.Response,
    latency time.Duration) {
    // some code
}

同氏は,Go言語には行の長さには制限がないため,コードをgrepすることを考えると1行にする方が良いと述べていました。

しかし同氏は,この場合であれば引数に簡潔な名前を用いることで,1行を短くすることは可能であると述べていました。また変数名などは,与えられたコンテキストの中でわかりやすい名前にすべきで,長い名前が必ずしもわかりやすい名前ではないと指摘していました。そして,以下のように冗長な名前は避けるべきだと述べていました。

  • samplingパッケージのSamplingServerは冗長である。
    • Serverという名前を付ければ,sampling.Serverとなる。
  • レシーバ変数は数文字でよい
  • 引数も型名から推測できる名前は付ける必要はなく,短くて良い。
  • 基本型の引数の場合は,わかりやすい名前にする。
  • ローカル変数も短くindexよりireaderよりrのほうが良い⁠⁠。
  • ローカル変数の名前を短くしても分かるように関数も小さくする。

上記の1行が長いコードは,名前を簡潔にすることで,以下のように短くなります。

package sampling

import (
    spb "foo/bar/service_proto"
)

type Server struct {
    // some fields
}

func (s *Server) SampleMetrics(req *spb.Request, resp *spb.Response, latency time.Duration) {
    // some code
}

著者プロフィール

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

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

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

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

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

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

Twitter:@tenntenn