deferの引数はdefer文を実行した時点の値で評価される
defer文に渡した関数呼び出しの引数は、実際に関数が実行される(関数の終了時)タイミングではなく、defer文自体が実行された時点で評価され確定します。
なぜエラーが出ないのか
出力: 1
(エラーなし)- Goは何も報告しません。コンパイルも実行も正常に終わるコードだからです
出力: 1- 実際の出力 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 package main 2 3 import "fmt" 4 5 func main() { 6 x := 1 7 defer fmt.Println(x) ^ 8 x = 2 9 }
defer fmt.Println(x)の引数xは、defer文が実行された時点(まだx=1の時点)で評価され確定してしまいます。関数終了時の最新の値を見たいなら、引数のないクロージャをdeferする必要があります。
直し方: defer fmt.Println(x) を defer func() { fmt.Println(x) }() にします。
パターン2
1 package main 2 3 import "fmt" 4 5 func main() { 6 count := 10 7 defer fmt.Println(count) ^ 8 count = 20 9 }
deferの引数評価タイミングはdefer文に到達した瞬間なので、その後countをいくら変更しても出力には反映されません。
直し方: defer fmt.Println(count) を defer func() { fmt.Println(count) }() にします。
パターン3
1 package main 2 3 import "fmt" 4 5 func main() { 6 n := 5 7 defer fmt.Println(n) ^ 8 n = 50 9 }
クロージャでラップすれば、変数nそのものを参照するようになり、関数終了時の最新値が使われます。
直し方: defer fmt.Println(n) を defer func() { fmt.Println(n) }() にします。
パターン4
1 package main 2 3 import "fmt" 4 5 func main() { 6 score := 0 7 defer fmt.Println(score) ^ 8 score = 100 9 }
deferに直接式の結果を渡す書き方は、その場で評価が完了してしまうため、後から値が変わっても反映されません。
直し方: defer fmt.Println(score) を defer func() { fmt.Println(score) }() にします。
パターン5
1 package main 2 3 import "fmt" 4 5 func main() { 6 level := 1 7 defer fmt.Println(level) ^ 8 level = 9 9 }
「deferは関数の一番最後の状態を見るはず」という直感は、引数を直接渡す書き方には当てはまりません。
直し方: defer fmt.Println(level) を defer func() { fmt.Println(level) }() にします。
よくある誤解
「defer fmt.Println(x)と書けば、関数の一番最後のxの値が出力されるはず」という思い込みは誤りです。引数の評価はdefer文に到達した瞬間に行われるため、その後xの値を変更しても出力には反映されません。値を変更したいなら引数のないクロージャをdeferする必要があります。
まとめ
deferの引数はdefer文を実行した時点の値で評価されるは上級でつまずきやすい項目です。上の5パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。