Go の :=
は非常に便利なのですが、特に多値を扱うときにハマる可能性があるので、よく理解して使いましょう。
基本
まずは次のコードを見てください。(Playground)
package main
import "fmt"
func main() {
a := 1
// a := 2 // これはコンパイルエラー
a, b := 3, 4 // a は定義ではなくただの代入
if b := true; b { // この b は if スコープで定義される
a, c := 5.5, 7 // これらも if スコープで定義
fmt.Println("a=", a, ", b=", b, ", c=", c)
}
fmt.Println("a=", a, ", b=", b)
}
まとめると:
-
:=
は左辺に最低一つ新しい変数の定義が無いとエラーになる。 - ブロックスコープ内で
:=
を使うと、ブロック外の変数を shadowing する形で新しい変数を定義する - ブロックスコープは
{}
の間だけでなく、 if や switch などのキーワードと{
の間も含まれる。
イディオム
基本さえ理解してしまえば、実際に使うのは C++ や Java と比べて特に難しいことはありません。
スコープを最小限に
可能なら if や switch の内側での定義を活用し、変数名の衝突を避ける。
頻出イディオムは慣習に従う
val, err := someFunc()
if err != nil {
return err
}
// val を使った処理
などの場合、 err
を if のスコープにはできません。
とはいえ、 bool 型の ok, error 型の err など、頻出するイディオムに対する変数名は慣習に従うことで、その後の val, err := ...
などの場面で再定義によるコンパイルエラーを避けられます。
:=
を避ける
:=
を使っていて怖いのは次のようなケースです。
func foo(v int) (int, error) {
ret := defaultValue()
if pred(v) {
// ret に代入するつもりが再定義
if ret, err := someFunc(v); err != nil {
log.Println("ret=", ret) // コンパイラの未使用変数チェックもすり抜ける
return -1, err
}
}
return ret, nil
}
val や関数の戻り値変数を使って、スコープの先頭で変数を定義し、 :=
を避ける事ができます。
func foo(v int) (ret int, err error) {
ret = defaultValue()
if pred(v) {
if ret, err = someFunc(v); err != nil {
log.Println("ret=", ret)
return -1, err
}
}
return
}
定義と初期化をセットにするのは、とくに一度初期化したらあとは参照するだけで再代入しない変数に対してはとても良い習慣ですが、再代入される変数はスコープの先頭で定義するのも悪くないと思います。型も明示できますしね。