
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
代入文(Assignment statements)
代入は、変数に格納されている現在の値を、式で指定された新しい値に置き換える。代入文は、単一の値を単一の変数に、または複数の値を対応する数の変数に代入することができる。
Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .
左辺の各オペランドは、アドレス指定可能であるか、mapのインデックス式であるか、(= による代入の場合のみ)ブランク識別子でなければならない。オペランドは括弧で囲むことができる。
x = 1
*p = f()
a[i] = 23
(k) = <-ch // k = <-ch と同じ
代入操作 x op= y(ここで op は二項算術演算子)は x = x op (y) と等価であるが、x は一度だけ評価される。op= 構文は単一のトークンである。代入操作では、左辺と右辺の式リストはどちらもちょうど1つの単一値の式を含まなければならず、左辺の式はブランク識別子であってはならない。
a[i] <<= 2
i &^= 1<<n
タプル代入は、複数の値を返す操作の個々の要素を変数のリストに代入する。2つの形式がある。1つ目は、右辺のオペランドが関数呼び出し、チャネルまたはmap操作、または型アサーションのような単一の複数値式である場合。左辺のオペランドの数は値の数と一致しなければならない。たとえば、f が2つの値を返す関数の場合、
x, y = f()
は最初の値を x に、2番目の値を y に代入する。2つ目の形式では、左辺のオペランドの数は右辺の式の数と等しくなければならず、各式は単一値でなければならず、右辺の n 番目の式が左辺の n 番目のオペランドに代入される:
one, two, three = '一', '二', '三'
ブランク識別子は、代入において右辺の値を無視する手段を提供する:
_ = x // x を評価するが無視する
x, _ = f() // f() を評価するが2番目の結果値を無視する
代入は2つのフェーズで進行する。第1に、左辺のインデックス式とポインタ間接参照(セレクタにおける暗黙的なポインタ間接参照を含む)のオペランド、および右辺の式がすべて通常の順序で評価される。第2に、代入が左から右の順序で実行される。
a, b = b, a // a と b を交換する
x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2 // i = 1, x[0] = 2 を設定
i = 0
x[i], i = 2, 1 // x[0] = 2, i = 1 を設定
x[0], x[0] = 1, 2 // x[0] = 1 を設定、次に x[0] = 2(最終的に x[0] == 2)
x[1], x[3] = 4, 5 // x[1] = 4 を設定、次に x[3] = 5 の設定でパニック
type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7 // x[2] = 6 を設定、次に p.x = 7 の設定でパニック
i = 2
x = []int{3, 5, 7}
for i, x[i] = range x { // i, x[2] = 0, x[0] を設定
break
}
// このループの後、i == 0 かつ x は []int{3, 5, 3}
代入において、各値は代入先のオペランドの型に代入可能でなければならない。ただし以下の特殊なケースがある:
- 任意の型付き値をブランク識別子に代入できる。
- 型なし定数がインターフェース型の変数またはブランク識別子に代入される場合、定数はまず暗黙的にそのデフォルト型に変換される。
- 型なし真偽値がインターフェース型の変数またはブランク識別子に代入される場合、まず暗黙的に
bool型に変換される。
値が変数に代入されるとき、変数に格納されているデータのみが置き換えられる。値が参照を含む場合、代入は参照をコピーするが、参照されるデータ(スライスの基底配列など)のコピーは行わない。
var s1 = []int{1, 2, 3}
var s2 = s1 // s2 は s1 のスライス記述子を格納する
s1 = s1[:1] // s1 の長さは 1 だが、基底配列はまだ s2 と共有している
s2[0] = 42 // s2[0] を設定すると s1[0] も変わる
fmt.Println(s1, s2) // [42] [42 2 3] を出力
var m1 = make(map[string]int)
var m2 = m1 // m2 は m1 のmap記述子を格納する
m1["foo"] = 42 // m1["foo"] を設定すると m2["foo"] も変わる
fmt.Println(m2["foo"]) // 42 を出力
解説
代入の基本
代入文は Go で最もよく書く文のひとつです。
x = 1 // 変数に値を代入
*p = f() // ポインタの先に値を代入
a[i] = 23 // スライス/配列の要素に代入
m["key"] = 42 // mapの要素に代入
複合代入演算子(+= など)
x += y は x = x + y と同じ意味ですが、x が一度だけ評価される点が異なります。
x += 5 // x = x + 5
x *= 2 // x = x * 2
a[i] <<= 2 // a[i] = a[i] << 2
i &^= 1<<n // i = i &^ (1<<n)
「一度だけ評価」がなぜ重要かというと、左辺に副作用のある式がある場合に違いが出るからです。
a[expensive()] += 1
// expensive() は1回だけ呼ばれる
// もし a[expensive()] = a[expensive()] + 1 と書いたら
// expensive() が2回呼ばれてしまう
タプル代入
複数の値を一度に代入できます。2つの形式があります。
形式1:複数値を返す式
x, y = f() // 関数の複数戻り値
v, ok = m["key"] // mapアクセスの2値
v, ok = <-ch // チャネル受信の2値
v, ok = x.(int) // 型アサーションの2値
形式2:右辺に複数の式
a, b, c = 1, 2, 3
one, two, three = '一', '二', '三'
値の交換が1行で書ける
タプル代入の便利な使い方として、値の交換があります。
a, b = b, a // 一時変数なしで a と b を交換!
他の多くの言語では一時変数が必要ですが、Go ではこれが1行で書けます。
// 他の言語で必要な書き方
tmp := a
a = b
b = tmp
// Go なら1行
a, b = b, a
代入の2フェーズ実行
代入が安全に動く仕組みを理解しましょう。代入は2つのフェーズで実行されます。
フェーズ1:すべての式を評価する(左辺のインデックスも右辺の値も) フェーズ2:左から右の順に代入を実行する
a, b = b, a がうまく動くのは、フェーズ1で b と a の値が両方とも先に評価されるからです。
// フェーズ1:右辺の b と a の現在の値を評価
// フェーズ2:a に旧 b の値を代入、b に旧 a の値を代入
ただし、フェーズ2は左から右に順番に実行されるので、同じ変数に2回代入すると最後の値が残ります。
x[0], x[0] = 1, 2 // まず x[0]=1、次に x[0]=2 → 最終的に x[0]==2
ブランク識別子で値を捨てる
不要な戻り値を _ で捨てるのは Go の定番パターンです。
_, err := os.Open("file.txt") // ファイルハンドルは不要、エラーだけ欲しい
_ = expensiveFunc() // 副作用だけ欲しいが、戻り値は不要
参照型の代入は「浅いコピー」
ここが非常に重要です。スライス、map、チャネルなどの参照型を代入すると、参照(記述子)がコピーされるだけで、中身はコピーされません。
s1 := []int{1, 2, 3}
s2 := s1 // 参照のコピー。中身は共有!
s2[0] = 42
fmt.Println(s1) // [42 2 3] ← s1 も変わった!
これを視覚的に表すとこうなります。
s1 → [ptr, len=3, cap=3] ──┐
├──→ [1, 2, 3] ← 基底配列を共有
s2 → [ptr, len=3, cap=3] ──┘
s2 を変更すると s1 にも影響するのは、同じ基底配列を指しているからです。独立したコピーが欲しい場合は明示的にコピーします。
s2 := make([]int, len(s1))
copy(s2, s1) // 中身もコピー
// または Go 1.21 以降
s2 := slices.Clone(s1)
mapも同様です。
m1 := map[string]int{"a": 1}
m2 := m1 // 参照のコピー
m1["a"] = 99
fmt.Println(m2["a"]) // 99 ← m2 も変わった!
この挙動はパフォーマンスのためです。巨大なスライスやmapを関数に渡すたびに全データをコピーしていたら、非常に遅くなってしまいます。Go は意図的にこの設計を選んでいますが、プログラマはこの共有の仕組みを理解しておく必要があります。
おわりに
本日は、Go言語の言語仕様について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)

コメント