Try Golang! ローカル変数のスコープに注意すべし
Three points to know about local variables scope in Golang.
Goにおけるローカル変数のスコープについて、コードを書く上で気を付けておきたいことを、3つまとめてみました。随所にJavaやPHPの比較があるのは、筆者のバックグラウンドの影響です。ご容赦下さい。
ちなみにここでの「ローカル変数」とは、関数やメソッドの中で宣言される変数のことを指しているので、パッケージ内の構造体や関数のスコープは本記事のスコープ外です。
0. 基本的な考え方
ローカル変数のスコープは、基本的に
変数の宣言以降、宣言したブロックの終端まで
です。
これはJavaとは同じ考え方なので、大変しっくりきます。if
やfor
で自然と作成されるブロック以外にも、意図的にブロックだけ作ることもできます(まぁあまりしないでしょうが)。(サンプルコード)
PHPの場合、関数単位のスコープとなるので、なが~い関数だと「ん?この変数使ってるかな?」なんてこともあったり。スコープは小さく分割できた方がいいですよね。
1. 簡易ステートメントで宣言した変数のスコープ
Goのif
やswitch
では、条件式の前に簡単なステートメントを記載することができ、ここで変数の宣言が可能です。Java等、他の言語でもfor
の頭にint i = 0
と書くことが多いかと思いますが、Goはこの簡易ステートメントをif
やswitch
でも書くことができます。
if
文の簡易ステートメントで宣言した変数は、if
ブロックでもelse
ブロックでも参照できます。Javaの場合、両方のブロックで使用したいものはそれぞれ宣言が必要でしたが、Goでは不要ということですね。なお、簡易ステートメントで宣言した変数は、if
ブロック、else
ブロックの外では当然参照できませんし、if
ブロック内で明示的に宣言した変数はelse
ブロックでは参照できません。(サンプルコード)
Goでは例外が発生した場合、error
を返してハンドリングを行いますが、関数の戻り値がerror
のみの場合は、以下のように書いてあげると、スコープを最小限にできますね。
if err := foo(); err != nil {
panic(err.Error())
}
2. 変数が上書きされる!?
他の静的言語でも同様ですが、同じ変数名で宣言することはできません。ですので、以下のコードはコンパイルエラーになります。
func main() {
var a int
a := 1 // error: no new variables on left side of :=
fmt.Println(a * 2)
}
しかし、以下のように少し修正してやると、コンパイルエラーにならずに実行することが可能です。
func main() {
var a int
if true {
a := 1
fmt.Println(a * 2)
}
fmt.Println(a * 2)
}
ちなみに実行結果はこんな感じ。
$ go run main.go
2
0
if
ブロックによってスコープが分かれ、かつ同名の変数が宣言されると、ブロックの内部では変数が上書きされたかのような挙動になります。ただ、そのブロックを抜けると元の変数を参照することになるので、上の例ではint
型の初期値である0
になっているわけです。
正直この仕様はいかがなものかと思いますが、例えばerr
といった変数名は何度も使うことにもなるので、その利便性を考慮してのことなのかなと思ったり。ただ、よほどの理由がない限り、同一関数内の変数名はユニークにすべきだとは思います。
3. 複数戻り値の型推論における宣言
関数を呼出してその戻り値を変数に代入する際にも、型推論による宣言が可能です。Goでは戻り値を複数返すことができますが、この場合も同様です。では、代入する変数の一方は宣言未済で、もう一方は宣言済の場合、コンパイルは通るのでしょうか?
func foo() (string, string) {
return "a", "b"
}func main() {
var a string
a, b := foo()
fmt.Printf("a='%s', b='%s'", a, b)
}
実際に動かすと一目瞭然ですが、上記のコードはエラーにはなりません。この時、実際に型推論されるのは、宣言未済の変数b
だけです。
なお、代入する変数の全てが宣言済の場合はエラーになるため、:=
ではなく=
に修正してあげて下さい。また、宣言済の変数の型と関数の戻り値の型が不一致の場合もエラーになります。
上でも書きましたがerr
という変数名はよく使用します。関数の戻り値が複数の場合、data, err := foo()
といった型推論の形式で複数回呼出してあげても、エラーにならないのはそういった理由なのです。
直感的に理解できるものも多いので、あまり意識せずにコードを書いている人も多いのではないかと思いますが、思いがけないところでハマってしまわまないためにも、基礎知識として頭の片隅に置いておきましょう。