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


いいなと思ったら応援しよう!

この記事が参加している募集