🗂

GO 言語の defer の評価順について確認してみた

2025/01/06に公開

引き続き GO 言語に入門しています。
今回は GO 言語の中で気になった defer について確認したことをまとめます。

defer キーワードの動作

GO 言語には、それが書かれた行を遅延実行する defer キーワードがあります。
https://go.dev/blog/defer-panic-and-recover

例示します。

main.go
package main

import "fmt"

func main() {
  defer fmt.Println("World")

  fmt.Println("Hello ")  
}

例えば上記のようなコードの場合、先に Hello が引数に渡された行が実行されるため、
ターミナルには次のように表示されます。

$ go run main.go
Hello
World

defer が付いたほうが後から実行されますね。

File を開き、いろいろな処理をしたあとにクローズするのを defer をつけて先に実行することで閉じ忘れないようにするなどの使い方が一般的かもしれません。

someFile, _ := os.OpenFile("./README.md", os.O_RDWR|os.O_CREATE, 0o666)
defer someFile.Close()

変数の評価

変数の評価はいつのタイミングで行うか気になったので次のコードで試します。

main.go
package main

import "fmt"

func main() {
  x := 10
  defer fmt.Println("Deferred x:", x)

  x = 20
  fmt.Println("Current x:", x)
}

実行すると下記のようになりました。

$ go run main.go
Current x: 20
Deferred x: 10

初期値として10を与えられた変数 x は、defer キーワードの行で一旦値を記録・評価し、
20を代入されたことで、Current x: 20 としたあとに
記録された値を出力したことがわかります。

単純なスキップではなく、スキャンしたタイミングで値を持ちつつ、すべての通常関数の実行完了後に記録時点での値を持って実行するわけですね。
遅延実行時点での評価ではないことに注意しようと思いました。

for 文でも確認してみます。

main.go
package main

import "fmt"

func main() {
  for i := 0; i < 3; i++ {
    defer fmt.Println(i)
  }
}

こちらを実行すると、予想通り、i の値を評価しつつ最後のカウントから遡って実行(fmt.Println(i))していることがわかります。
LIFO: Last In, First Outの性質があることがわかりました。

$ go run main.go
2
1
0

関数をまたいだのときの動作

関数がまたいで defer を使っている場合はどうでしょうか。

main.go
package main

import "fmt"

func deferFunc() {
  fmt.Println("Defer function")
}

func main(){
  fmt.Println("running")
  defer fmt.Println("World World")
  defer deferFunc()
  fmt.Println("test")
}

実行すると

$ go run main.go
running
test
Defer function
World World

となります。
deferFuncが実行されたあと、fmt.Println("World World")が実行される動きは変わりません。

また、たとえば deferFunc に変数 x を渡して、defer の中身を書き換えるような処理であっても
素直にLIFOの動作でした。
(引数を書き換えちゃう関数がいいかは置いておいて...)

main.go
package main

import "fmt"

func deferFunc(x int) {
  fmt.Println("Defer function")
  defer fmt.Println("deferFunc last x:", x)
  x = 20
  fmt.Println("deferFunc first x:", x)
}

func main() {
  var x int = 10
  fmt.Println(x)
  fmt.Println("running")
  defer deferFunc(x)
}
$ go run main.go
10
running
Defer function
deferFunc first x: 20
deferFunc last x: 10

Return があるときの動作

値を返さない関数の動作はわかりました。
次に Return があるときはどうか確認します。

main.go
package main

import "fmt"

func example() int {
  defer fmt.Println("Deferred")
  fmt.Println("Returning")
  return 42
}

func main() {
  fmt.Println("Result:", example())
}

実行結果は以下のようになります。

$ go run main.go
Returning
Deferred
Result: 42

Returning が表示された後、 return キーワードまで到達し、関数が終了するタイミングで defer の行が実行されるような動作でした。

Panic があるときの動作

エラーを発生した場合の動作のときはどうでしょうか。
故意に panic を起こして様子を見てみます。

main.go
package main

import "fmt"

func example() int {
  panic("Panic")
}

func main() {
  fmt.Println("Start")
  defer func() {
    recover()
    fmt.Println("Recovered")
  }()

  fmt.Println("Result:", example())
  fmt.Println("End")
}

実行結果は以下のようになります。

$ go run main.go
Start
Recovered

example() がエラーを起こし終了するのを recover() で復帰し、 fmt.Println("Recovered") で正常に抜けるような動作でした。
panic -> recover での大域脱出のため、fmt.Println("End")には到達しません。

まとめ

  • deferはスキャン(記録)は実行時に行われる。
  • 実行は関数終了時まで遅延され、記録された順番とは逆順(LIFO)で実行される。
  • 引数の評価は記録時点で行われる。

Discussion