打ち首こくまろ

タイトルは適当

【Go】変数宣言とブロックが織り成す罠

またしてもハマりまくったのでメモ。

Go言語では以下のように変数を宣言する。

var osi string

ここではosiというstring型の変数を宣言した。当然、この変数にはいろいろな値を入れて、取り出すことができる。

var osi string
osi = "真壁瑞希"
fmt.Println(osi)
// ->真壁瑞希

ただ、いちいちvarと書くのは面倒なので、Goでは:=という省略された変数宣言の方法が用意されている。

osi := "真壁瑞希"
fmt.Println(osi)
// ->真壁瑞希

Goは賢いので、この変数はstring型ですよとわざわざ明記しなくても、:=を使うと自動で判別してくれるので便利。

ところで、varを使っての変数の再宣言をすることはできない。

var osi string
var osi int
// -> tmp/sandbox263718883/main.go:8:6: osi redeclared in this block
//     previous declaration at tmp/sandbox263718883/main.go:7:6

:=も同様。同じ変数に対して二度以上:=を使うことはできない(例外はあるが割愛)。

osi := "真壁瑞希"
osi := "北沢志保"
// -> tmp/sandbox285020658/main.go:9:5: no new variables on left side of :=

ここまでが前提。

以下のコードを考えてみる。

osi := "真壁瑞希"
if 1 == 1 {
    osi = "北沢志保"
    fmt.Println(1, osi)
}
fmt.Println(2, osi)

このコードを実行するとどうなるだろうか?

当然、以下が出力される。

1 北沢志保
2 北沢志保

ifブロックの中でosi北沢志保が代入されるため、print文では二つとも北沢志保が出力される。

では問題のコードである。

osi := "真壁瑞希"
if 1 == 1 {
    osi := "北沢志保"
    fmt.Println(1, osi)
}
fmt.Println(2, osi)

osiは既に宣言されているのにもかかわらず、北沢志保の代入に:=を使ってしまっている。

であるならば、当然エラーが出力されるはず…

1 北沢志保
2 真壁瑞希

ファッ!?

エラーが出ないどころか、二つ目のprint文では真壁瑞希が復活している。殺したはずでは。。。

Goの日本語ガイドでは以下のように書いてある。

ブロック内で宣言された識別子は、内側のブロック内で再宣言できます。

内側のブロック内で宣言した識別子がスコープ内にある間、その識別子は内側で宣言した実体を表しつづけます。

つまり、ifブロックやforブロック内で宣言した変数は、外側の変数とは全く無関係の別物になる。

よって、上記のコードは以下のコードと等価。

osi := "真壁瑞希"
if 1 == 1 {
    saiosi := "北沢志保"
    fmt.Println(1, saiosi)
}
fmt.Println(2, osi)

=:=を取り違えただけで、全く違う動作になってしまった。

この仕様は控えめに言って頭おかしいとしか思えない。。。 なぜこんな治外法権のような動作を組んだのか意図が不明だし、直感的でもない。 さらに=:=はコード中で見分けがつきにくいため、これが原因のバグは結構見つけるのに苦労する(体験談)。

Goはシンプルでいい言語というのは概ね同意するけど、こういう喉に小骨が引っかかるような動作をそこかしこに仕込んでるの、おじさん良くないと思うなぁ。