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

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

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

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

defer文(Defer statements)

defer 文は、囲んでいる関数が return 文を実行したか、関数本体の末尾に到達したか、または対応するゴルーチンがパニックしているために、囲んでいる関数が戻る瞬間まで実行が遅延される関数を呼び出す。

DeferStmt = "defer" Expression .

式は関数またはメソッド呼び出しでなければならない。括弧で囲むことはできない。組み込み関数の呼び出しは式文と同様に制限される。

defer 文が実行されるたびに、呼び出しの関数値とパラメータは通常どおり評価されて新たに保存されるが、実際の関数は呼び出されない。代わりに、遅延された関数は囲んでいる関数が戻る直前に、遅延された順序の逆順で呼び出される。すなわち、囲んでいる関数が明示的な return 文によって戻る場合、遅延された関数はその return 文によって結果パラメータが設定された後、関数が呼び出し元に戻る前に実行される。遅延された関数値が nil と評価される場合、defer 文の実行時ではなく、関数が呼び出される時にパニックが発生する。

たとえば、遅延された関数が関数リテラルであり、囲んでいる関数がそのリテラルのスコープ内にある名前付き結果パラメータを持つ場合、遅延された関数は結果パラメータが返される前にアクセスして変更することができる。遅延された関数に戻り値がある場合、関数の完了時にそれらは破棄される。(パニックの処理の節も参照のこと。)

lock(l)
defer unlock(l)  // unlock は囲んでいる関数が戻る前に発生する

// 囲んでいる関数が戻る前に 3 2 1 0 を出力する
for i := 0; i <= 3; i++ {
	defer fmt.Print(i)
}

// f は 42 を返す
func f() (result int) {
	defer func() {
		// result は return 文によって 6 に設定された後にアクセスされる
		result *= 7
	}()
	return 6
}

解説

defer ってなに?

defer は、関数の終了時に実行したい処理を予約する仕組みです。「あとで必ずやってね」という指示をコンパイラに伝えます。

func readFile(name string) ([]byte, error) {
    f, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // この関数が終わるとき、必ず f.Close() を実行してね

    return io.ReadAll(f)
}

defer f.Close() と書いた瞬間に Close が呼ばれるわけではありません。関数が終了するタイミング(return やパニック時)まで実行が延期されます。

なぜ defer が必要なの?

defer がないと、リソースの解放を書き忘れるリスクがあります。

// defer なし:Close を書き忘れる危険がある
func readFile(name string) ([]byte, error) {
    f, err := os.Open(name)
    if err != nil {
        return nil, err
    }

    data, err := io.ReadAll(f)
    if err != nil {
        f.Close()        // ここで Close が必要
        return nil, err
    }

    f.Close()            // ここでも Close が必要
    return data, nil
}

// defer あり:一箇所書くだけで安全
func readFile(name string) ([]byte, error) {
    f, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // どのパスで抜けても必ず実行される

    data, err := io.ReadAll(f)
    if err != nil {
        return nil, err  // Close は defer が面倒見てくれる
    }
    return data, nil
}

引数は defer 文の時点で評価される

これは非常に重要なポイントです。defer に渡す関数の引数は、defer 文が実行された時点で評価されます。実際の呼び出し時ではありません。

func example() {
    x := 10
    defer fmt.Println(x)  // x=10 がここで評価されて保存される
    x = 20
}
// 出力: 10(20 ではない!)

fmt.Println(x)xdefer 文の時点で 10 と評価されるので、その後に x を変更しても影響しません。

LIFO(後入れ先出し)の順序

複数の defer がある場合、後に defer されたものが先に実行されます。スタック(積み重ね)のイメージです。

func example() {
    defer fmt.Println("1st")
    defer fmt.Println("2nd")
    defer fmt.Println("3rd")
}
// 出力:
// 3rd
// 2nd
// 1st

原文のループの例も同じ原理です。

for i := 0; i <= 3; i++ {
    defer fmt.Print(i)  // i=0, 1, 2, 3 の順で defer される
}
// 出力: 3 2 1 0(逆順で実行される)

名前付き戻り値の変更

defer の最も強力な使い方のひとつが、名前付き戻り値を defer 内で変更することです。

func f() (result int) {
    defer func() {
        result *= 7  // return で設定された result を変更できる
    }()
    return 6
}
// 戻り値: 42(6 × 7)

この挙動は以下の順序で起きます。

  1. return 6result6 に設定される
  2. defer された関数が実行される → result6 * 7 = 42 に変更される
  3. 関数が呼び出し元に 42 を返す

エラーハンドリングでの活用

名前付き戻り値と defer を組み合わせた実用的なパターンです。

func writeFile(name string, data []byte) (err error) {
    f, err := os.Create(name)
    if err != nil {
        return err
    }
    defer func() {
        closeErr := f.Close()
        if err == nil {
            err = closeErr  // 書き込みは成功したが Close で失敗した場合
        }
    }()

    _, err = f.Write(data)
    return err
}

Close のエラーを無視せずに適切に処理できています。

nil 関数値の defer

nil の関数値を defer すると、defer 文の時点ではパニックしませんが、実際に呼び出されるとき(関数の終了時)にパニックします。

func example() {
    var f func()  // nil
    defer f()     // ここではパニックしない
    fmt.Println("processing...")
}
// "processing..." が出力された後、defer の実行時にパニック

よくある使い方

1. リソースの解放

mu.Lock()
defer mu.Unlock()

f, _ := os.Open(name)
defer f.Close()

conn, _ := net.Dial("tcp", addr)
defer conn.Close()

2. パニックの回復

defer func() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}()

3. 実行時間の計測

func process() {
    start := time.Now()
    defer func() {
        fmt.Printf("処理時間: %v\n", time.Since(start))
    }()

    // 処理
    heavyWork()
}

4. トレースログ

func enter(name string) string {
    fmt.Println("entering:", name)
    return name
}

func leave(name string) {
    fmt.Println("leaving:", name)
}

func process() {
    defer leave(enter("process"))  // enter が先に評価される
    // 処理
}
// 出力:
// entering: process
// (処理)
// leaving: process

enterdefer 文の時点で呼ばれ(引数の評価)、leave は関数終了時に呼ばれます。

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

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

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

コメント

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