Golang:Defer, Panic, Recoverについて学習
defer
日本語で「延期する」。deferへ関数を渡すとstackに格納され、呼び出し元の関数がreturnするときに格納された関数が実行される。
使い道
クリーンアップ処理に用いる。OpenしたファイルのCloseし忘れを防げる。特に分岐してreturn文が存在するとき、Closeを書く箇所が1か所で済むので簡潔なコードになる。
deferを使う場合
package main
import (
"os"
"log"
)
func main() {
var file *os.File
var err error
file, err = os.Open("test.txt")
if err != nil {
log.Fatal(err)
return
}
defer file.Close() // ここだけでOK
// 適当な処理
return
}
deferを使わない場合
package main
import (
"os"
"log"
)
func main() {
var file *os.File
var err error
file, err = os.Open("test.txt")
if err != nil {
log.Fatal(err)
file.Close() // 分岐したreturnの箇所に書く必要あり
return
}
// 適当な処理
file.Close() // 当然こちらにも必要
return
}
3つのルール
1.deferへ渡した関数の引数はdefer文が評価されたときのものを用いる。
下記コードで出力されるのは0。
package main
import (
"fmt"
)
func main() {
i := 0
defer fmt.Println(i) // defer文が実行されるとき、iは0
i++
return
}
2.複数のdefer文が存在するとき、FILO順で関数は実行される
package main
import (
"fmt"
)
func main() {
for i := 0; i < 5; i++ {
fmt.Print(i)
}
fmt.Println()
for i := 0; i < 5; i++ {
defer fmt.Print(i)
}
}
$ go run main.go
01234
43210
defer文が実行されたのはfmt.Print(0)->fmt.Print(1)->fmt.Print(2)->fmt.Print(3)->fmt.Print(4)という順序だが、出力順は43210であり、FILOの順で出力されていることが分かる。
3.関数のnamed return valuesをreturn前に変更可能
named return valuesについてはこれとかこれを参照。
defer func(){}()の形で利用(?)。deferの無名関数内でnamed return valuesの値を変更することが出来る。
詳しい使い道はpanicの項目で説明。
package main
import (
"fmt"
)
func f() (i int) {
fmt.Printf("1:%d\n", i) // 0
defer func() {
fmt.Printf("2:%d\n", i) // 1
i = 100
fmt.Printf("3:%d\n", i) // 100
}()
return 1
}
func main() {
fmt.Printf("4:%d\n", f()) // 100
}
$ go run main.go
1:0
2:1
3:100
4:100
f()の返り値として、1ではなく、100が返っていることが分かる。
ちなみに、defer func内では、defer以前で定義された変数(named return values含む)は利用できるが、deferより後に定義された変数は利用できない。
panic
現行のgoroutineの通常の実行を止める。
関数fでpanicが発生した場合、panic以降のコードは実行されず、deferされた関数を順に処理し、関数fの処理を終了する。関数fの呼び出し元関数gでは、関数fをpanic呼び出しと同様に処理する。これは実行中のgoroutineのすべての関数が停止するまで続行される。
package main
import (
"fmt"
)
func f() {
fmt.Println("3")
panic("panic!")
fmt.Println("4")
}
func g() {
fmt.Println("2")
f()
fmt.Println("5")
}
func h() {
fmt.Println("1")
g()
fmt.Println("6")
}
func main() {
h()
}
$ go run main.go
1
2
3
panic: panic!
goroutine 1 [running]:
main.f()
/home/smihata/myworkspace/go_tutorial/main.go:9 +0x59
main.g()
/home/smihata/myworkspace/go_tutorial/main.go:15 +0x4f
main.h()
/home/smihata/myworkspace/go_tutorial/main.go:21 +0x4f
main.main()
/home/smihata/myworkspace/go_tutorial/main.go:27 +0xf
exit status 2
fでpanicが発生したため、fmt.Println("4")は実行されない。
fでpanicが発生したため、g内のf()はpanicとして扱うのと同様の処理になる。よってfmt.Println("5")は実行されない。
gでpanic発生と同様のことが発生したため、h内のg()はpanicとして扱うのと同様の処理になる。よってfmt.Println("6")は実行されない。
以上のことから、123のみが出力されている。
使い道
強制的に後続の処理を中断する。プログラムは実行を継続させるのが基本。また、panicは配列の範囲外参照などでも発生する。
参照:https://qiita.com/nayuneko/items/9534858156dfd50b43fb
下記のようにdefer func()とセットで使用することで、panic発生時の処理を記述することができる。
基本的には使用するべきではない。
package main
import (
"fmt"
)
func f() () {
defer func() {
fmt.Println("3")
}()
fmt.Println("1")
panic("an error occurred")
fmt.Println("2")
}
func main() {
f()
}
$ go run main.go
1
3
panic: an error occurred
goroutine 1 [running]:
main.f()
/home/smihata/myworkspace/go_tutorial/main.go:12 +0x78
main.main()
/home/smihata/myworkspace/go_tutorial/main.go:17 +0xf
exit status 2
また、Go1.21以降は、nilインターフェース値、または型が指定されていないnilを渡すと専用のrun time errorが発生する。
package main
func main() {
var err error
panic(err) // nilインターフェース値
panic(nil) // 型が指定されていないnil
}
$ go run main.go
panic: panic called with nil argument
goroutine 1 [running]:
main.main()
/home/smihata/myworkspace/go_tutorial/main.go:5 +0x17
exit status 2
recover
defer func内でrecoverを呼び出すことで通常の処理を復元し、panicに渡されたエラーメッセージや値をrecoverが取得し、panicの処理を停止する。
defer func外でrecoverが呼び出された場合、panicの処理を停止しない。つまり、defer func内でのみ使用される。
defer func外でrecoverが呼び出されたり、panicが発生しなかった場合、recoverはnilを返す。
使い道
基本的にpanic同様使用しない。
g()内でpanic発生以降のコードは実行されていない。そして、panicの引数がrecover()の返り値に入っていることが分かる。また、gを呼び出したf内ではgはpanicとして扱われていないことが分かる。
package main
import "fmt"
func g() {
defer func() {
if r := recover(); r!= nil {
fmt.Println("recover!", r)
}
}()
panic("panic!")
fmt.Println("2")
}
func f() {
fmt.Println("1")
g()
fmt.Println("3")
}
func main() {
f()
}
$ go run main.go
1
recove
参考:https://go.dev/blog/defer-panic-and-recover