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

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

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

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

スポンサーリンク

背景

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

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

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

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

パニックの処理(Handling panics)

2つの組み込み関数 panicrecover は、実行時パニックおよびプログラムで定義されたエラー条件の報告と処理を支援する。

func panic(interface{})
func recover() interface{}

関数 F の実行中に、panic への明示的な呼び出しまたは実行時パニックが F の実行を終了する。F によって遅延されたすべての関数は、その後通常どおり実行される。次に、F の呼び出し元によって実行される遅延関数が実行され、実行中のゴルーチンのトップレベル関数によって遅延されたものまで同様に続く。その時点で、プログラムは終了し、panic への引数の値を含むエラー条件が報告される。この終了シーケンスはパニックと呼ばれる。

panic(42)
panic("unreachable")
panic(Error("cannot parse"))

recover 関数は、パニックしているゴルーチンの動作をプログラムが管理できるようにする。関数 Grecover を呼び出す関数 D を遅延し、G が実行されているのと同じゴルーチン上の関数でパニックが発生したとする。遅延関数の実行が D に到達したとき、Drecover 呼び出しの戻り値は panic の呼び出しに渡された値になる。D が新しい panic を開始せずに正常に戻れば、パニックシーケンスは停止する。その場合、Gpanic の呼び出しの間に呼び出された関数の状態は破棄され、通常の実行が再開される。D の前に G によって遅延されたすべての関数が実行され、G の実行は呼び出し元に戻ることで終了する。

ゴルーチンがパニックしていないか、recover が遅延関数から直接呼び出されていない場合、recover の戻り値は nil である。逆に、ゴルーチンがパニックしており recover が遅延関数から直接呼び出された場合、recover の戻り値は nil でないことが保証される。これを保証するため、nil のインターフェース値(または型なしの nil)で panic を呼び出すと実行時パニックが発生する。

以下の例の protect 関数は、関数引数 g を呼び出し、g によって引き起こされる実行時パニックから呼び出し元を保護する。

func protect(g func()) {
	defer func() {
		log.Println("done")  // パニックがあっても Println は正常に実行される
		if x := recover(); x != nil {
			log.Printf("run time panic: %v", x)
		}
	}()
	log.Println("start")
	g()
}

解説

panic と recover の基本

panicプログラムを異常終了させる関数、recoverパニックを捕まえて正常な実行に戻す関数です。

// パニックを起こす
panic("something went wrong")

// パニックを捕まえる
defer func() {
    if r := recover(); r != nil {
        fmt.Println("パニックを捕まえた:", r)
    }
}()

他の言語の throw/catch や例外処理に似ていますが、Go では通常のエラーハンドリングには error を使い、panic/recover本当に回復不能な異常事態のためだけに使います。

パニックの伝播の仕組み

パニックが起きると、以下の順序で処理が進みます。

1. パニックが発生した関数 F の実行が停止
2. F の defer が逆順に実行される
3. F の呼び出し元に戻り、その defer が実行される
4. さらに上の呼び出し元に戻り...
5. ゴルーチンのトップまで到達するとプログラムが終了
func main() {
    defer fmt.Println("main の defer")  // 3番目に実行
    f()
}

func f() {
    defer fmt.Println("f の defer")     // 2番目に実行
    g()
}

func g() {
    defer fmt.Println("g の defer")     // 1番目に実行
    panic("問題発生!")
}

// 出力:
// g の defer
// f の defer
// main の defer
// panic: 問題発生!

defer はパニック中でも必ず実行されるという点が重要です。リソースの解放が確実に行われることが保証されます。

recover の使い方

recoverdefer された関数の中でのみ機能します。それ以外の場所で呼んでも nil が返るだけです。

// ✅ 正しい使い方:defer の中で直接呼ぶ
defer func() {
    if r := recover(); r != nil {
        fmt.Println("回復:", r)
    }
}()

// ❌ 効果なし:defer の外で呼んでも nil が返るだけ
r := recover()  // nil(パニック中でないか、defer 内でない)

// ❌ 効果なし:defer の中で間接的に呼んでもダメ
defer func() {
    handlePanic()  // この中で recover() を呼んでも nil が返る
}()

recover が「遅延関数から直接呼び出される」必要がある、というのが原文のポイントです。

panic(nil) は実行時パニックを起こす

recover の戻り値が nil のとき「パニックしていない」と判断できるよう、panic(nil) は特別扱いされます。

panic(nil)  // 実行時パニック!(nil でのパニックは禁止)

もし panic(nil) が許されたら、recover()nil を返したときに「パニックしていない」のか「nil でパニックした」のか区別できなくなるからです。

protect パターン

原文の protect 関数は、「任意の関数を安全に実行する」パターンです。

func protect(g func()) {
    defer func() {
        log.Println("done")
        if x := recover(); x != nil {
            log.Printf("run time panic: %v", x)
        }
    }()
    log.Println("start")
    g()
}

// 使い方
protect(func() {
    // ここでパニックが起きても、プログラムは終了しない
    riskyOperation()
})

g() がパニックしても、protect の defer 内で recover が捕まえるため、protect の呼び出し元には影響しません。

よくある使い方

1. HTTP サーバーでのパニック回復

func handler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("handler panic: %v", err)
            http.Error(w, "Internal Server Error", 500)
        }
    }()
    
    processRequest(r)  // ここでパニックしてもサーバーは死なない
}

2. ゴルーチンの保護

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("goroutine panic: %v", r)
        }
    }()
    
    doWork()  // パニックしても他のゴルーチンに影響しない
}()

ゴルーチン内でパニックが発生すると、recover しない限りプログラム全体が終了します。バックグラウンド処理では recover を入れておくのが安全です。

panic と error の使い分け

Go のエラーハンドリングには明確な使い分けがあります。

errorpanic
用途予期されるエラー予期されない致命的エラー
ファイルが見つからない、ネットワーク障害nil ポインタ参照、範囲外アクセス
伝播方法戻り値として返すコールスタックを巻き戻す
処理方法if err != nildefer + recover
// error を使うべき場面(予期されるエラー)
func readFile(name string) ([]byte, error) {
    return os.ReadFile(name)
}

// panic を使うべき場面(プログラムのバグ)
func mustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic("invalid regex: " + pattern)  // プログラムのバグなので panic
    }
    return re
}

Go のコミュニティでは、ライブラリが panic を外部に漏らすべきではないとされています。ライブラリ内で panic を使っても、公開 API の境界で recover して error に変換するのが作法です。

おわりに 

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

よっしー
よっしー

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

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

コメント

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