LoginSignup
2
2

More than 5 years have passed since last update.

go言語の複数戻り値とscopeのハマりどころ

Posted at

はじめに

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以下だと最初からcursor0が入るので、一見正しく動いてるように見えてしまうところですね。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2