0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Golang 〜Defer, Panic, and Recover〜

Last updated at Posted at 2021-04-17

Defer

関数呼び出しをリストに Push する。周囲の関数を実行後、リストに貯まった関数が実行される。
Defer は、様々な Clean up 処理に使用される。

サンプルコード

ファイルをコピーする下記のコードを例にする。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

このサンプルコードは動くものの、バグが潜在する。os.Create(dstName) が失敗すると、コピー元のファイルを close することができない。
このバグは、以下のように src に対して明示的に Close() を呼び出すことで解決できる。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

Defer によって、各ファイルをオープン後に close することが保証される。

3つのルール

1. Defer 関数の引数は、Defer ステートメントが評価される時に評価される

main.go
func a() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

func main() {
	b()
}
$ go run main.go 
0

上記を実行すると、0が出力される。iは、Println()が call された際に評価されることがわかる。

2. Defer 関数の呼び出しは、 LIFO(Last In First Out) にて実行される

main.go
func b() {
	for i := 0; i < 4; i++ {
		defer fmt.Println(i)
	}
}
func main() {
	b()
}
$ go run main.go 
3
2
1
0

3. Defer 関数は、関数の戻り値を読み取り、新しい値を割り当てることができる

main.go
func c() (i int) {
	defer func() { i++ }()
	return 1
}

func main() {
	fmt.Println(c())
}
$ go run main.go 
2

上記の例では、c()の返り値 i に1を足す Defer 関数を作成している。よって、2が返ってくる。
3のルールは、エラー時の挙動を決定するにあたり、非常に有用である。

Panic

Panic() は、通常の制御の流れを止めて Panic を開始する組み込み関数である。関数FがPanicを呼び出すと、Fの実行が停止し、F内のすべての Defer 関数が正常に実行されてから、Fは呼び出し元に戻る。 Caller にとって、Fはパニックへの呼び出しのように動作する。プロセスは、現在の goroutine (Goのランタイムによって管理される軽量スレッド)内のすべての関数が戻るまで Stack を上っていく。戻ると、プログラムがクラッシュする。
Panic は、 Panic を直接呼び出すことによって開始できる。また、範囲外の配列アクセスなどのランタイムエラーが原因で発生することもある。

Recover

Recover は、 Panic 状態の goroutine の制御を取り戻す組み込み関数である。 Recover は、 Defer 関数内でのみ使用可能。通常の実行中、 Recover の呼び出しは nil を返し、他の効果はない。現在の goroutine が Panic になっている場合、Recoverの呼び出しは Panic に与えられた値をキャプチャし、通常の実行を再開する。

サンプルコード

main.go
package main

import "fmt"

func main() {
	f()
	fmt.Println("Returned normally from f.")
}

func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
		}
	}()
	fmt.Println("calling g.")
	g(0)
	fmt.Println("Returned nomally from g.")
}

func g(i int) {
	if i > 3 {
		fmt.Println("panicking!!!")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("Defer in g", i)
	fmt.Println("Printing in g", i)
	g(i + 1)
}
$ go run main.go 
calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
panicking!!!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Recover() がなかった場合、以下のように goroutine のスタック最上部まで戻り、プログラムを終了させる。

main.go
package main

import "fmt"

func main() {
	f()
	fmt.Println("Returned normally from f.")
}

func f() {
	// defer func() {
	// 	if r := recover(); r != nil {
	// 		fmt.Println("Recovered in f", r)
	// 	}
	// }()
	fmt.Println("calling g.")
	g(0)
	fmt.Println("Returned nomally from g.")
}

func g(i int) {
	if i > 3 {
		fmt.Println("panicking!!!")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("Defer in g", i)
	fmt.Println("Printing in g", i)
	g(i + 1)
}
$ go run main.go 
calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
panicking!!!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4

(StackTrace は省略。。。)

Panic と Recover の実例として、Go標準ライブラリのjsonパッケージを参照すると良い。jsonパッケージは、一連の再帰関数を使用してインターフェースをエンコードする。値のトラバース中にエラーが発生した場合、 Panic() が呼び出されてスタックが最上位の関数呼び出しに巻き戻され、 Panic から回復して適切なエラー値が返される(encode.go)。

Goライブラリの規則では、パッケージが内部で Panic を使用している場合でも、その外部 API は明示的なエラー戻り値を表示する、としている。

他に、 Defer の使用例として、 Mutex のリリースや、フッターの出力などがある。

mu.Lock()
defer mu.Unlock()
printHeader()
defer printFooter()

参考

Defer, Panic, and Recover

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?