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

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

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

本日は、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言語の言語仕様について解説しました。

よっしー
よっしー

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

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

コメント

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