Go言語学習その29~エラー処理その2:panicとrecover

前回 (https://note.com/yasmizohawks/n/n444b24fe9ac6) に引き続き
Go におけるエラー処理を見てみます。


■ panic で強制終了

致命的な技術的エラーや
データベースのデータがあってはならないものになっているなどで
アプリケーションの処理を強制終了させて
原因の調査や対応を行いたいことがあるかと思います。

「アプリケーションを強制終了させる」機能として
Go には panic 関数が用意されています。

以下にサンプルを示します。

package main

import (
	"fmt"
)

func sampleProcess() {
	fmt.Println("■ sampleProcess:処理開始")
	panic("■ sampleProcess:処理が継続できないエラーが出たので強制終了")
	fmt.Println("■ sampleProcess:処理終了")
}

func main() {
	fmt.Println("■ main:処理開始")
	sampleProcess()
	panic("■ main:処理が継続できないエラーが出たので強制終了")
	fmt.Println("■ main:処理終了")
}

サンプルでは、main 関数と main 関数から呼ばれる sampleProcess 関数の
両方で panic 関数を実行しています。

上記サンプルの実行結果は以下になります。

■ main:処理開始
■ sampleProcess:処理開始
panic: ■ sampleProcess:処理が継続できないエラーが出たので強制終了

goroutine 1 [running]:
main.sampleProcess()
C:/golang/study/src/error/err02.go:9 +0x65
main.main()
C:/golang/study/src/error/err02.go:15 +0x57
exit status 2

実行結果を見ると
・main 関数から sampleProcess 関数の呼び出しは行われている
・sampleProcess 関数で panic 関数が実行されている
・sampleProcess 関数での panic 関数実行以降の処理は実行されていない
・main 関数に処理が戻ってない
ことがわかります。

つまり、
・panic 関数が実行された場所がどこであろうと、
 実行された場所でアプリケーションが強制終了する
ということになりますね。

システム開発を行う際に共通関数を作成することがありますが
共通関数内でうかつに panic 関数を実行するようにしてしまうと
共通関数を呼び出した側に処理が戻らないため
呼び出した側でのロールバック処理などのエラー対応処理が実行されない
などの問題が出そうですね。

Go では panic 関数の利用は推奨されてないようですが
使う場合はソースコード全体の構成や
panic 関数を実行する場所の検討はしっかりやっておきたいところですね。

■ recover

panic は、panic 関数で意図的に発生させることもできますが、
意図していない、想定外の panic が発生することもあります。
たとえば、利用したライブラリが panic を発生させるものだったり
その他実行時に何か想定外の panic が発生する可能性もありますね。

想定外の panic が発生した場合に
・何かエラー対応をしたい
・アプリケーションを強制終了させずに処理を継続したい
ことがあると思います。

そういった場合には recover 関数を使います。
以下にサンプルを示します。

package main

import (
	"fmt"
	"errors"
)

func sampleProcess() (e error) {

	// deferで無名関数即時実行
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("■ sampleProcess:recover処理")
			fmt.Println("■ sampleProcess:recoverの戻り値=", r)
			e = errors.New("panic出たよ")
		}
	}()

	fmt.Println("■ sampleProcess:処理開始")
	panic("■ sampleProcess:panic 実行")
	fmt.Println("■ sampleProcess:処理終了")

	e = nil
	return
}

func main() {
	fmt.Println("■ main:処理開始")
	
	e := sampleProcess()
	
	if e != nil {
		fmt.Println("■ main:sampleProcess実行でエラーが出ました")
		fmt.Println(e)
	} else {
		fmt.Println("■ main:sampleProcess実行は正常終了しました")
	}
	
	fmt.Println("■ main:処理終了")
}

上記サンプルで recover 関数を使っているコードは以下になります。

defer func() {
 if r := recover(); r != nil {
  fmt.Println("■ sampleProcess:recover処理")
  fmt.Println("■ sampleProcess:recoverの戻り値=", r)
  e = errors.New("panic出たよ")
 }
}()

fmt.Println("■ sampleProcess:処理開始")
panic("■ sampleProcess:panic 実行")
fmt.Println("■ sampleProcess:処理終了")

recover 関数は defer と組み合わせて使います。
まず、
panic("■ sampleProcess:panic 実行")
部分で panic を発生させています。
panic が発生すると defer で定義してある無名関数が実行されますが、
無名関数内で以下の条件判定をしています。

if r := recover(); r != nil

defer で定義した関数は、panic が発生してもしなくても動きます。
recover 関数は、panic が発生した場合は panic 関数に渡した値
( 文字列 "■ sampleProcess:panic 実行" )
を返却し、
panic が発生していない場合は nil を返却します。

つまり、recover 関数の戻り値が nil でない場合は
panic が発生していると判断できるので

if r := recover(); r != nil {【recover処理】}

として、recover 関数の戻り値が nil でない場合に
【recover処理】を実行する、というサンプルイメージにしています。

上記サンプルの実行結果は以下になります。

■ main:処理開始
■ sampleProcess:処理開始
■ sampleProcess:recover処理
■ sampleProcess:recoverの戻り値= ■ sampleProcess:panic 実行
■ main:sampleProcess実行でエラーが出ました
panic出たよ
■ main:処理終了

実行結果を見ると
・main 関数から sampleProcess 関数の呼び出しが行われている
・sampleProcess 関数で recover 処理が実行されている
・recover 関数の戻り値が panic 関数に渡した文字列になっている
 ⇒ panic 関数による panic 発生がきっかけで recover 処理が動いてる
・sampleProcess 関数内の panic 関数実行以降の処理は実行されていない
・main 関数に処理が戻っている
ことがわかります。

sampleProcess 関数内では、panic 関数を実行している行の
次の行以降が実行されていないので
panic により sampleProcess 関数の実行は強制終了され
recover 処理に処理が移行しています。

そして main 関数に処理が戻っているので
main 関数の処理は強制終了されていませんね。
sampleProcess 関数呼び出し以降も
main 関数では処理が継続できることになります。

前述の「■ panic で強制終了」とは違い、
panic 発生したけれどアプリケーションは強制終了させていないわけです。

panic ~ recover の使いどころとしては、たとえば
データベースアクセス処理を行う関数において
panic 発生の場合にロールバック
したいといったときに使うことになりそうですね。

#プログラミング
#IT
#プログラミング言語
#Go言語
#GO
#Golang

この記事が気に入ったらサポートをしてみませんか?