はじめに
go言語で複数の戻り値を使った変数への代入と、scopeの扱いでバグを出してしまったので恥ずかしながらその供養に。
例題
いきなりですが、よくプログラミングの課題であるやつ
A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA
をあえて無限ループを使って作ってみた以下のコードは正しく動くでしょうか。
package main
import (
"fmt"
)
func main() {
str := "A"
current := 1
for {
output, current := appendAndIncrement(str, current)
fmt.Println(output)
if current > 10 {
break
}
}
}
// 元文字列と繰り返しの数 n を受け取り、n だけ繰り返された文字列とインクリメントされた n を返す。
func appendAndIncrement(src string, n int) (str string, incr int) {
for i := 0; i < n; i++ {
str += src
}
incr = n + 1
return
}
試しにやってみるとcurrent
がインクリメントされずにループを抜け出せないことがわかります。
https://play.golang.org/p/hCjSEGzw3Qc
分かってしまえば理由は単純で、output
を新規変数として宣言しようとして誤ってcurrent
まで新規に宣言してしまっています。goでは内側のscope(この場合はfor
の中)内で変数の再宣言が可能なためです。
ブロック内で宣言された識別子は、内側のブロック内で再宣言できます。内側のブロック内で宣言した識別子がスコープ内にある間、その識別子は内側で宣言した実体を表しつづけます。
普段は特に意識しなくても自然に使っている仕様ですが、複数戻り値が絡んだ際に意図せず変数の再宣言をしてしまったのが上のコードでした。正しく動作させるにはfor
の中身を以下のように修正しないといけません。
for {
var output string // 個別に宣言する
output, current = appendAndIncrement(str, current) // 既存変数への再代入
fmt.Println(output)
if current > 10 {
break
}
}
どんな時にハマったか
redisのSCAN機能を使ってkeyのリストを取り出したい時に、うっかり以下のようなコードを書くと無限ループにハマって死にます。
var cursor uint64
var keys []string
var err error
for {
tmpKeys, cursor, err := redisdb.Scan(cursor, "key*", 20).Result()
if err != nil {
panic(err)
}
keys = append(keys, tmpKeys...)
if cursor == 0 {
break
}
}
fmt.Println("key length:", len(keys))
タチが悪いのは、テスト環境などでredisのkeyが20以下だと最初からcursor
に0
が入るので、一見正しく動いてるように見えてしまうところですね。