
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
評価順序(Order of evaluation)
パッケージレベルでは、初期化の依存関係が変数宣言における個々の初期化式の評価順序を決定する。それ以外の場合、式、代入、または return 文のオペランドを評価する際、すべての関数呼び出し、メソッド呼び出し、受信操作、および二項論理演算は、字句的に左から右の順序で評価される。
たとえば、以下の(関数ローカルな)代入において
y[f()], ok = g(z || h(), i()+x[j()], <-c), k()
関数呼び出しと通信は f()、h()(z が false と評価された場合)、i()、j()、<-c、g()、k() の順序で発生する。ただし、これらのイベントと x の評価およびインデックス付け、ならびに y と z の評価との間の順序は、字句的に要求される場合を除き、規定されない。たとえば、g はその引数が評価される前に呼び出されることはできない。
a := 1
f := func() int { a++; return a }
x := []int{a, f()} // x は [1, 2] または [2, 2] の可能性がある: a と f() の間の評価順序は規定されない
m := map[int]int{a: 1, a: 2} // m は {2: 1} または {2: 2} の可能性がある: 2つのmap代入の間の評価順序は規定されない
n := map[int]int{a: f()} // n は {2: 3} または {3: 3} の可能性がある: キーと値の間の評価順序は規定されない
パッケージレベルでは、初期化の依存関係が個々の初期化式に対する左から右のルールを上書きするが、各式内のオペランドに対しては上書きしない:
var a, b, c = f() + v(), g(), sqr(u()) + v()
func f() int { return c }
func g() int { return a }
func sqr(x int) int { return x*x }
// 関数 u と v は他のすべての変数および関数から独立している
関数呼び出しは u()、sqr()、v()、f()、v()、g() の順序で発生する。
単一の式内の浮動小数点演算は、演算子の結合性に従って評価される。明示的な括弧はデフォルトの結合性を上書きすることで評価に影響を与える。式 x + (y + z) では、加算 y + z が x を加える前に実行される。
解説
なぜ評価順序が重要なの?
ほとんどの場合、評価順序を気にする必要はありません。しかし、副作用のある式(関数呼び出しや変数の変更など)が絡むと、順序によって結果が変わる可能性があります。
a := 1
f := func() int { a++; return a }
// a と f() のどちらが先に評価される?
x := []int{a, f()}
// a が先なら [1, 2]、f() が先なら [2, 2]
Go の仕様では、こういったケースの順序は**規定されない(未規定)**とされています。「未定義動作」(何が起きてもよい)とは違い、結果は2つの候補のどちらかに確定しますが、どちらになるかは保証されません。
保証されること:関数呼び出しは左から右
Go が保証してくれるのは、関数呼び出し・メソッド呼び出し・チャネル受信・論理演算は左から右に評価されるということです。
原文の複雑な例を順番に追ってみましょう。
y[f()], ok = g(z || h(), i()+x[j()], <-c), k()
左から右に、関数呼び出し等を拾っていきます。
f()—yのインデックスとして最初に現れるz || h()— 論理OR。zがfalseのときだけh()が呼ばれる(短絡評価)i()— 次の関数呼び出しj()—x[j()]の中の関数呼び出し<-c— チャネル受信g()— 引数がすべて揃ったのでgを呼ぶk()— 最後の関数呼び出し
ただし、変数 x、y、z の評価(関数呼び出しではない単なる変数アクセス)がどのタイミングで行われるかは規定されません。
保証されないこと:単純な式の評価順序
関数呼び出し以外の式(変数アクセス、インデックス計算など)の評価順序は保証されません。
a := 1
f := func() int { a++; return a }
x := []int{a, f()}
a の評価と f() の呼び出しのどちらが先かは未規定です。こういうコードは書くべきではありません。意図を明確にするには、分けて書きましょう。
a := 1
f := func() int { a++; return a }
// 順序を明確にする
aVal := a
fVal := f()
x := []int{aVal, fVal} // 確実に [1, 2]
パッケージレベルの初期化順序
パッケージレベルの変数は、依存関係によって初期化順序が決まります。左から右ではありません。
var a, b, c = f() + v(), g(), sqr(u()) + v()
func f() int { return c } // f は c に依存
func g() int { return a } // g は a に依存
依存関係を整理すると:
cはu()とv()だけに依存(他の変数に依存しない)→ 最初に計算aはf()とv()に依存、f()はcに依存 →cの後に計算bはg()に依存、g()はaに依存 →aの後に計算
結果として、関数呼び出しの順序は:
u()→sqr()→v()(cの初期化)f()→v()(aの初期化)g()(bの初期化)
浮動小数点の結合性
浮動小数点演算では、括弧が計算結果に影響を与えることがあります。
x + (y + z) // y + z を先に計算してから x を足す
(x + y) + z // x + y を先に計算してから z を足す
x + y + z // 左から右に結合 → (x + y) + z と同じ
浮動小数点数には丸め誤差があるため、演算の順序によって結果が微妙に変わることがあります。
var a float64 = 1e20
var b float64 = -1e20
var c float64 = 1.0
fmt.Println((a + b) + c) // 1.0(1e20 - 1e20 = 0 → 0 + 1 = 1)
fmt.Println(a + (b + c)) // 0.0(-1e20 + 1 ≈ -1e20 → 1e20 + (-1e20) = 0)
同じ式でも括弧の位置で結果が変わります。精度が重要な計算では、括弧で演算順序を明示するのが良い習慣です。
実用上のアドバイス
評価順序に関して覚えておくべきことは3つだけです。
1. 副作用のある式を複合リテラルや引数リストに混ぜない
// 悪い例:結果が予測できない
x := []int{a, f()} // a と f() のどちらが先かわからない
// 良い例:順序を明確にする
val := f()
x := []int{a, val}
2. 関数呼び出しの順序は左から右と覚える
fmt.Println(f(), g(), h()) // f → g → h の順で呼ばれることが保証される
3. 迷ったら分けて書く
一行に複数の副作用を詰め込むよりも、複数行に分けて書くほうが読みやすく安全です。コンパイラの最適化に任せれば、パフォーマンスの差はほとんどありません。
おわりに
本日は、Go言語の言語仕様について解説しました。

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

コメント