Goでよくやってしまうミスが3つあります。
私はそのミスを、分かりやすいように簡略した書き方ではなく、巷でよく見かける書き方のままここで説明します。
3つのミスの全てが、私の知る限り少なくとも1回づつ、Kubernetesの過去のコードレビューにありました。
1: ループ変数がループ外のスコープになっている
この各行はいったい何を行っているのでしょうか。想像してから下へスクロールしてください。
func print(pi *int) { fmt.Println(*pi) }
for i := 0; i < 10; i++ {
defer fmt.Println(i)
defer func(){ fmt.Println(i) }()
defer func(i int){ fmt.Println(i) }(i)
defer print(&i)
go fmt.Println(i)
go func(){ fmt.Println(i) }()
}
解答:
func print(pi *int) { fmt.Println(*pi) }
for i := 0; i < 10; i++ {
defer fmt.Println(i) // OK: 9 ... 0 を出力します
defer func(){ fmt.Println(i) }() // NG: "10" を10回出力します
defer func(i int){ fmt.Println(i) }(i) // OK
defer print(&i) // NG: "10" を10回出力します
go fmt.Println(i) // OK: 0 ... 9 をランダムな順に出力します
go func(){ fmt.Println(i) }() // NG: 完全に予測不可能
}
for key, value := range myMap {
// Same for key & value as i!
}
みなさんはこれらの変数がループ内のスコープになっていると思うでしょうが、
Goは全てのイテレーションで同じメモリ領域を使用します。
このことは上の key
や value
, i
のアドレスはループから逃げることが出来ないということを意味しています。
無名関数(クロージャ) func() { /* do something with i */ }
は変数のアドレスを取る微妙な方法です。
2: nilインターフェースはインターフェースのnilポインタとは違うこと
type Cat interface {
Meow()
}
type Tabby struct {}
func (*Tabby) Meow() { fmt.Println("にゃー") }
func GetACat() Cat {
var myTabby *Tabby = nil
// おおっと、myTabbyに値を代入し忘れました
return myTabby
}
func TestGetACat(t *testing.T) {
if GetACat() == nil {
t.Errorf("本物のネコを返却し忘れています!")
}
}
どうなるでしょうか? 上記のテストはnilポインタを検出しません!
これはインターフェースがポインタの役目を果たすので、GetACatは事実上、nilポインタへのポインタを返却します。
GetACatのようなことをしないでください。そうすればあなた(と同僚)はもっと幸せになれます。
これはerror型の値で とても 簡単に起こります。
http://golang.org/doc/faq#nil_error
3: あなたの正気を隠す有害なもの
var ErrDidNotWork = errors.New("失敗しました")
func DoTheThing(reallyDoIt bool) (err error) {
if reallyDoIt {
result, err := tryTheThing()
if err != nil || result != "成功しました" {
err = ErrDidNotWork
}
}
return err
}
上の関数は 必ず nilエラーを返却します。
内部のerr変数が関数スコープ変数を「隠してしまう」からです。
var result string
を追加し、 :=
を使うのをやめれば修正されます。