
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
関数リテラル(Function literals)
関数リテラルは無名関数を表す。関数リテラルは型パラメータを宣言できない。
FunctionLit = "func" Signature FunctionBody .
func(a, b int, z float64) bool { return a*b < int(z) }
関数リテラルは変数に代入することも、直接呼び出すこともできる。
f := func(x, y int) int { return x + y }
func(ch chan int) { ch <- ACK }(replyChan)
関数リテラルはクロージャである。すなわち、外側の関数で定義された変数を参照することができる。それらの変数は外側の関数と関数リテラルの間で共有され、アクセス可能である限り存続する。
解説
関数リテラルってなに?
関数リテラルは、名前のない関数をその場で作る書き方です。他の言語では「無名関数」「ラムダ式」「アロー関数」などと呼ばれるものに相当します。
通常の関数宣言と比べてみましょう。
// 通常の関数宣言:名前がある
func Add(x, y int) int {
return x + y
}
// 関数リテラル:名前がない
func(x, y int) int {
return x + y
}
違いは名前があるかないかだけです。関数リテラルには func の後に関数名がありません。
変数に代入して使う
名前がないので、そのままでは呼び出せません。変数に代入してから使うのが一般的です。
f := func(x, y int) int {
return x + y
}
result := f(3, 5) // 8
こうすると、f が関数を指す変数になります。普通の関数と同じように f(3, 5) で呼び出せますね。
その場で直接呼び出す
変数に入れず、作ったその場ですぐ呼び出すこともできます。
func(ch chan int) { ch <- ACK }(replyChan)
// ^^^^^^^^^^
// ここが引数。作った直後に呼んでいる
もう少しわかりやすい例で見てみましょう。
result := func(x, y int) int {
return x + y
}(3, 5) // 作ってすぐ呼ぶ → result は 8
このパターンは、一度しか使わない処理をその場で実行したいときに便利です。
型パラメータは使えない
関数リテラルではジェネリクス(型パラメータ)を使えません。
// 通常の関数宣言ならOK
func min[T ~int|~float64](x, y T) T { ... }
// 関数リテラルではNG
f := func[T ~int|~float64](x, y T) T { ... } // コンパイルエラー!
ジェネリックな処理が必要な場合は、通常の関数宣言を使いましょう。
クロージャ:外側の変数を「覚えている」
関数リテラルの最も強力な特徴がクロージャです。外側の関数で定義された変数を、関数リテラルの中からそのまま使えます。
func makeCounter() func() int {
count := 0 // 外側の変数
return func() int {
count++ // 外側の count を参照・変更できる!
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
makeCounter の実行が終わっても、count は消えません。返された関数リテラルがまだ count を参照しているからです。原文の「アクセス可能である限り存続する」とはこのことです。
「共有される」ことに注意
外側の変数はコピーではなく共有されます。これは便利ですが、落とし穴にもなります。
よくあるミスを見てみましょう。
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
funcs[i] = func() {
fmt.Println(i) // i を共有している
}
}
funcs[0]() // 3 !?
funcs[1]() // 3 !?
funcs[2]() // 3 !?
すべて 3 になります。3つの関数リテラルが同じ i を共有しているため、ループが終わった後の i の値(= 3)が出力されるのです。
意図通りに 0, 1, 2 を出力するには、ループ変数をクロージャに「閉じ込める」必要があります。
for i := 0; i < 3; i++ {
i := i // 新しい i を作ってコピーする(シャドーイング)
funcs[i] = func() {
fmt.Println(i) // この i はループごとに別の変数
}
}
funcs[0]() // 0
funcs[1]() // 1
funcs[2]() // 2
i := i という一見不思議な行は、ループ変数の i を新しいローカル変数の i にコピーしています。これにより、各関数リテラルがそれぞれ別の i を持つようになります。Go でクロージャを使うときの定番テクニックなので、ぜひ覚えておいてください。
おわりに
本日は、Go言語の言語仕様について解説しました。

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

コメント