
こんにちは。よっしーです(^^)
本日は、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) の x は defer 文の時点で 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)
この挙動は以下の順序で起きます。
return 6→resultが6に設定される- defer された関数が実行される →
resultが6 * 7 = 42に変更される - 関数が呼び出し元に
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
enter は defer 文の時点で呼ばれ(引数の評価)、leave は関数終了時に呼ばれます。
おわりに
本日は、Go言語の言語仕様について解説しました。

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

コメント