変数宣言と代入の基本
宣言後の値の更新は常に =
。:=
/var
は宣言専用。
記法 | 目的 | 右辺は必須か | 使える場所 | 初期化の挙動 | 同一スコープでの再宣言 |
---|---|---|---|---|---|
:= |
新規宣言+初期化 | 必須 | 関数内のみ | 右辺の値で必ず初期化 | 不可(※複数代入で新規が1つ以上あれば文としてはOK) |
= |
代入のみ | 必須 | どこでも(宣言済みに限る) | —(宣言しないので該当なし) | —(宣言しないので該当なし) |
var |
宣言(+任意で初期化) | 任意 | どこでも | 右辺なし→ゼロ値/右辺あり→その値で初期化 | 不可 |
- ゼロ値
- 変数を初期化式なしで宣言したときに自動で入る既定値。型ごとに決まっています
- 初期化
- 宣言時に値をセットする行為全般で、ゼロ値でのセットも“初期化”に含まれます
コード例
// var: 宣言(右辺なしならゼロ値)
var x int // 0
// :=: 新規宣言+初期化(関数内)
y := 10
// =: 宣言済みに代入
y = 20
避けたい書き方(再代入と新規が入り混じっているため)
a := 1
b := 2
a := 3 // NG: 新規が1つも無いので再宣言不可
a, b := 3, 4 // NG: 新規が1つも無いので再宣言不可
a, c := 5, 6 // OK: aは代入、cは新規(少なくとも1つ新規)
シャドーイングの落とし穴
シャドーイング:外側と同名の新しい変数を内側で宣言して、外側を隠すこと
避けるべき理由
- バグの温床
- エラー処理の取りこぼし
- 可読性、レビュー性の低下
package main
import "fmt"
func main() {
x := 10
if true {
x := x + 10
fmt.Println("x:", x) // 20 になる
}
fmt.Println("x:", x) // 10 のまま
}
典型バグ1:if の初期化子で外の変数を“更新したつもり”
f, err := os.Open("in.txt")
if err != nil { return err }
// ここ、f を更新したつもりが…
if f, err := os.Open("out.txt"); err != nil { // ← 新しい f, err を宣言(内側だけ有効)
return err
}
defer f.Close() // ← 外側の f を close。内側の f は既にスコープ外
避け方
- 外で必要な変数は外で宣言してから = で代入する。
var (
f *os.File
err error
)
f, err = os.Open("out.txt") // ← 再代入
if err != nil { return err }
defer f.Close()
典型バグ2:if err := ...; err != nil の“err シャドー”
err := doA()
if err != nil { return err }
// ここで別の err を作る
if err := doB(); err != nil { // ← 内側専用の err
return err
}
// ここに戻ると外側の err に戻る(doB の結果は残らない)
避け方
- 一貫して同じ
err
を再利用する(=
で代入) - もしくは名前を分ける(berr など)。静的解析ツールでも検出しやすい
if err = doB(); err != nil { return err }
典型バグ3:ビルトインをシャドー(len, new など)
len := 10 // 以後、len(...) が呼べない
_ = len
指針:組み込み名は変数名に使わない。(type
, string
, error
, nil
も避ける)
まとめ
新規は :=
、更新は =
、グローバルやゼロ値・型明示は var
、そして内側で同名を作らない(シャドー禁止)——これだけ守れば宣言・代入・スコープ起因の事故はほぼ防ぐことができます。