Go の defer: 引数はいつ評価され、どの順で実行され、戻り値はどう変わるか

Go の defer は defer 文の実行時に引数を評価し、関数が戻る直前に逆順で呼び出します。名前付き結果パラメータを変更できる理由まで、言語仕様に基づいて整理します。

godeferlanguage-specclosures

Go で defer f(x) と書くと、f は後で呼び出されます。しかし x は後ではなく、その場で評価されます。この違いのせいで、ログに想定と違う値が出たり、return 3 を実行した関数から呼び出し側が 30 を受け取ったりします。この記事では、Go の言語仕様に書かれた規則だけを使ってこの動作を説明します。

規則のまとめ

Go 仕様の「Defer statements」節が定めている内容は次のとおりです。

  • defer の後には関数呼び出しかメソッド呼び出しが必要で、括弧で囲むことはできません。
  • defer 文が実行された時点で、関数値と引数が評価されて保存されます。関数そのものはこの時点では呼び出されません。
  • 保存された呼び出しは、外側の関数が戻る直前に実行されます。戻り方は return 文の実行、関数本体の末尾への到達、そのゴルーチンのパニックのいずれも含みます。
  • 複数の defer は、登録した順の逆順で実行されます。
  • 明示的な return で戻る場合、まず return が結果パラメータに値を設定し、次に defer された関数が実行され、最後に呼び出し側へ戻ります。

Effective Go は、メソッドの場合はレシーバも引数と一緒に defer の時点で評価されると補足しています。また、defer はブロック単位ではなく関数単位で動作します。if ブロックの中で defer しても、そのブロックの終わりではなく外側の関数が戻るときに実行されます。

例

次のプログラムは三つの規則をまとめて示します。

package main

import "fmt"

func show(label string, v int) {
	fmt.Println(label, v)
}

func compute() (n int) {
	x := 1
	defer show("argument:", x)
	defer func() { show("closure:", x) }()
	defer func() { n *= 10 }()
	x = 2
	return 3
}

func main() {
	fmt.Println("result:", compute())
}

仕様の規則から導いた出力は次のとおりです。この環境には Go ツールチェーンがなかったため、実際に実行した結果ではありません。

closure: 2
argument: 1
result: 30

順に追うと次のようになります。

  1. 最初の defer show("argument:", x) が実行されるとき、x は 1 です。この 1 が引数として保存されます。
  2. 二つ目の defer は引数のない関数リテラルを保存します。関数リテラルはクロージャなので、外側の関数の変数 x をコピーせずに共有します。そのため、このクロージャが後で実行されるときは、その時点の x を読みます。
  3. 三つ目の defer は、結果パラメータ n を 10 倍にするクロージャを保存します。
  4. x = 2 が実行されます。
  5. return 3 が n に 3 を設定します。
  6. defer された呼び出しが逆順に実行されます。まず n が 30 になり、次にクロージャが現在の x である 2 を出力し、最後に保存しておいた引数 1 で show が呼び出されます。
  7. compute は n の値 30 を返します。

argument: 1 と closure: 2 の違いが要点です。引数は defer の時点で評価された値が保存されるため、この例では整数 1 が使われます。クロージャの中で読む変数は実行時点の値になります。ただし、引数の保存はディープコピーではありません。ポインタ、スライス、マップのように別のデータを参照する値を渡した場合、保存されるのはその参照の値なので、参照先が後で変更されると、defer された呼び出しからも変更後の内容が見えることがあります。

名前付き結果パラメータを変更できる理由

仕様の「Return statements」節によると、名前付きの結果パラメータは通常のローカル変数として振る舞い、関数に入った時点でその型のゼロ値に初期化されます。そして、結果を指定した return 文は、defer された関数が実行される前に結果パラメータを設定します。

そのため、defer された関数リテラルからその結果パラメータが見える位置にあれば、すでに設定された戻り値を読んだり変更したりできます。仕様にも、return 6 の後に defer されたクロージャが result *= 7 を実行し、関数が 42 を返す例があります。同じ仕組みで、結果パラメータ err に名前を付けておけば、defer されたクロージャで err を確認して別の値に置き換えることもできます。

逆に、結果に名前がなければ、defer された関数が参照できる変数名がないため、この方法で戻り値を変えることはできません。また、defer された関数自身が値を返しても、その値は捨てられます。defer された関数の戻り値で外側の関数の結果を変えることはできません。

見落としやすい点

  • nil の関数値: defer された関数値が nil の場合、パニックは defer 文の時点ではなく、その関数を呼び出そうとする時点で起きます。
  • ループ内の defer: 繰り返しのたびに呼び出しが一つずつ積まれ、ループの終わりではなく関数が戻るときにまとめて逆順で実行されます。仕様の例 for i := 0; i <= 3; i++ { defer fmt.Print(i) } は 3、2、1、0 の順に出力します。
  • 引数の評価時点: 後の値が必要なら、引数として渡さずにクロージャの中で変数を読みます。今評価した値を使いたいなら、引数として渡します。ただし、ポインタ、スライス、マップを渡しても、参照先のデータのコピーは作られません。

範囲と出典

この記事は、2026-10-13 に確認した Go 言語仕様(ページ表記:Language version go1.27, May 26, 2026)に基づいています。各規則がどのバージョンから仕様にあったかは扱いません。recover、defer の性能、コンパイラの最適化などの実装の詳細も扱いません。例の出力は仕様の規則から導いたもので、実行結果ではありません。

以下のリンクは 2026-10-13 に確認しました。

ブログへ