Go 言語の defer 文

ここでは Go 言語の defer 文について説明します。

Go の defer 文とは?

defer 文は次の形で使います。

defer 関数(またはメソッド)呼び出し

defer 文で関数を呼び出すと、その関数の呼び出しは、 「defer 文を実行した関数を抜ける時に」呼び出されます

さっそく、具体例を見てみましょう。

package main

import "fmt"

func main() {
	fmt.Println("+main()")
	defer fmt.Println("Hello, defer!")
	fmt.Println("-main()")
}
main.go

この実行結果は次のようになります。

go run main.go
+main()
-main()
Hello, defer!

7行目で defer 文を使っています。

この例では main() 関数に入った直後の6行目で、 fmt.Println() で+main() という文字を出力しています。

次の7行目、defer 文で Hello, defer! という文字列を渡して fmt.Println() を呼び出しています。

そして最後に main() 関数を抜ける前に -main() という文字を出力しています。

-main() という文字は main 関数の最後の行で呼び出したのですが、実際にはそれが最後に実行されたのではなく、 defer 付きの Hello, defer! という文字の出力の方が最後に呼ばれていることがわかります。

このように、defer 文で関数を呼ぶと、defer 文を実行した関数やメソッドのブロックを抜ける時に、defer 文で呼んだ関数が実行されます。

Go の defer 文は、いつ使うの?

defer 文は、「関数ブロックを抜ける前に必ずしておきたい処理」がある場合に使うと良いでしょう。

関数ブロックを抜ける前に必ずしておきたい処理というのは、例えば、ファイルを開いた後のファイルハンドルなどです。

func readFile() {
	f, err := os.Open("main.go") // オープン
	if err != nil {
		panic(err)
	}
	defer f.Close() // クローズ (defer)

	// それからファイルを使う
	// (これでファイルの閉じ忘れはない)
}

具体的に、ファイルの閉じ忘れを避けるには、ファイルをオープンした箇所と対になるようにクローズが記載されている方がわかりやすいです。

また、クローズする箇所が特定の条件の元で実行されない、という場合もありますので、関数を抜けた時に自動的に呼び出されるという仕組みがあると便利です。

このように、オープンとクローズをペアに記載することでの閉じ忘れ防止、さらに、関数ブロックを抜けた時に自動的に呼びされる、という二重の意味でリソースの解放に defer を使用することが適しています。

複数の defer 文の実行順序

defer 文で実行する関数が複数ある場合、 defer 文を実行した順番の逆の順で実行されます。

次の例をみてください。

package main

import "fmt"

func main() {
	fmt.Println("+main()")
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
	fmt.Println("-main()")
}
main.go

この実行結果は次のようになります。

go run main.go
+main()
-main()
3
2
1

defer 文実行時に、その呼び出し予定をプッシュしていき、defer 文を実行した関数を抜ける時にそのスタックからポップして、実際の関数呼び出しを実行するという状況になります。

defer 文で実行する関数への引数は先に評価される

これまで説明してきたように、 defer 文で関数を実行する場合は、その defer 文を実行した関数から抜けるまで関数の呼び出しは行われません。

しかし、その関数に渡すパラメータの評価は先に実施されます。このためパラメータの評価時に関数があれば、その関数は先に実行されます。

次の例を見てください。

package main

import "fmt"

func foo(m int) int {
	fmt.Println("+foo()")
	m = m * m
	fmt.Println("-foo()")
	return m
}

func main() {
	fmt.Println("+main()")
	m := 5
	defer fmt.Println(foo(m)) // 引数は bar() の戻り値
	m = 10
	fmt.Println("-main()")
}
main.go

ここでは15行目で defer 文で fmt.Println() を実行しています。引数には foo() 関数の戻り値を渡している点がポイントです。

この実行結果は次のようになります。

go run main.go
+main()
+foo()
-foo()
-main()
25

この実行結果からわかるように、 foo() 関数は main 関数が終わる前に (-main() がプリントされる前に) 実行済みです。

defer 文で実行が先送りされるのは、あくまでも defer で直接指定した関数のみであり、引数の評価は defer 文実行時点で行われます

以上、ここでは Go 言語の defer 文について説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Go 言語入門