Go言語入門:言語仕様 -Vol.107-

スポンサーリンク
Go言語入門:言語仕様 -Vol.107- 用語解説
Go言語入門:言語仕様 -Vol.107-
この記事は約9分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、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}

代入において、各値は代入先のオペランドの型に代入可能でなければならない。ただし以下の特殊なケースがある:

  1. 任意の型付き値をブランク識別子に代入できる。
  2. 型なし定数がインターフェース型の変数またはブランク識別子に代入される場合、定数はまず暗黙的にそのデフォルト型に変換される。
  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 += yx = 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で ba の値が両方とも先に評価されるからです。

// フェーズ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言語の言語仕様について解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました