Go月なので勝手にGo言語強化月間
前回のエラーハンドリングの記事の続きのようなものです。
→ Go言語のエラーハンドリングについて
まずはじめに結論から。
panic編と銘打っておいてなんですが、panicは基本的に使わないです。
じゃあなぜ書いたし
panicはGo言語におけるいわゆるランタイムエラーです。
Go言語でプログラムを書いているとそのうち嫌でもぶち当たると思います。
今回はpanicと愉快な仲間たちと向き合ってみましょう!
Go言語のもうひとつのエラー:panic
panicとは
ときおり、プログラムは全く継続できない事態に陥ることがあります。
こういったときのために、組み込み関数panicがあります。これは、実質的にプログラムを停止させるランタイムエラーを作成します。
panicとはプログラムの継続的な実行が難しく、どうしよもなくなった時にプログラムを強制的に終了させるために発生するエラーです。
panicを生成するpanic関数はbuilt-inに定義されています。
func panic(v interface{})
panicが生成されると、後続の処理を中断し、呼び出し元を辿り最終的にプログラムを終了させます。
いわゆる例外処理のthrowステートメントが呼び出された時のような振る舞いを行います。
package main
import "fmt"
func main() {
fmt.Println("main start")
nansuPanic()
fmt.Println("main end") // 呼ばれない
}
func nansuPanic() {
fmt.Println("nansuPanic start")
panic("(*>△<)<ナーンナーンっっ")
fmt.Println("nansuPanic end") // 呼ばれない
}
main start
nansuPanic start
panic: (*>△<)<ナーンナーンっっ
goroutine 1 [running]:
panic(0x4b7040, 0xc82006a210)
/usr/local/go/src/runtime/panic.go:481 +0x3e6
main.nansuPanic()
/tmp/go/errors/panic/main.go:13 +0x11e
main.main()
/tmp/go/errors/panic/main.go:7 +0xe3
本来ライブラリではpanicを起こさないようにしなければなりません。問題点を隠せるか回避できるのであれば、プログラム全体をダウンさせるより、実行を継続させる方が常に優れています。
安定してプログラムを運用するためにpanicをいかに発生させないかが重要になってきます。
panicとはどういう時に起きるのか
panicは上記のように故意に発生させることも可能ですが、基本的に自ら発生させることはないと思いますので、Go言語から発せられるpanicの芽を摘んでいくことが重要となってきます。
panicが発生するポイントは他言語で云う所のセグメンテーション違反やぬるぽのようなメモリ周りのエラーからゼロ除算やインデックス範囲外のようなWarning/Noticeレベルのものまで様々ですが、基本的に事前チェックしておけば回避できるものばかりです。
下記に記す内容はほんの一部ですが、ここでは自身が遭遇した発生ポイントを挙げていきます。
sliceの範囲外を参照しようとしたとき
package main
import "fmt"
func main() {
text := "hoge"
fmt.Println("1:", text[0:2]) // これはOK
fmt.Println("2:", text[5:]) // panic発生!
fmt.Println("end") // 呼ばれない
}
1: ho
panic: runtime error: slice bounds out of range
goroutine 1 [running]:
panic(0x4d9500, 0xc82000a0a0)
/usr/local/go/src/runtime/panic.go:481 +0x3e6
main.main()
/tmp/go/errors/panic2/main.go:8 +0x41e
text変数の文字列をsliceを使って切り出す処理ですが、文字列が4文字なので、6文字目以降を切り出そうとしてpanicを起こしています。
事前に文字長チェックをするか文字長に合わせて取得範囲を選択する等して回避できます。
初期化されていないポインタの実体を参照しようとしたとき
package main
import "fmt"
func main() {
var p *int
fmt.Printf("%p\n", p)
fmt.Printf("%d\n", *p)
}
0x0
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x4011a4]
goroutine 1 [running]:
panic(0x4d9420, 0xc82000a0e0)
/usr/local/go/src/runtime/panic.go:481 +0x3e6
main.main()
/tmp/go/errors/panic3/main.go:8 +0x1a4
ちょっと強引ですが(笑)
int型のポインタで宣言したpの実体を参照しようとしたところでpanicを起こしています。
事前にnilチェックしておくことで回避可能です。
型アサーションに失敗
package main
import "fmt"
func main() {
var x interface{} = "hogehoge"
i := x.(int)
// i, ok := x.(int) とすることで変換に失敗した場合、ok変数にfalseが代入されpanicが発生しない
fmt.Println(i)
}
panic: interface conversion: interface is string, not int
goroutine 1 [running]:
panic(0x4d95e0, 0xc8200660c0)
/usr/local/go/src/runtime/panic.go:481 +0x3e6
main.main()
/tmp/go/errors/panic4/main.go:7 +0xa8
interface型で定義された変数xに代入された文字列をint型に型アサーションしようとして失敗し、panicが発生します。
事前に型のチェックをしておくか、型アサーションの成功可否を戻り値で受け取ることで回避可能です。
panicと愉快な仲間たち
panicをリカバーするrecover,defer
panicは、deferとrecoverを使ってリカバーすることが出来ます。
Go言語のdeferステートメントは、deferを実行した関数がリターンする直前に、指定した関数の呼び出しが行われるようにスケジューリングします。(→遅延指定)
deferステートメントというと、ファイルなどのリソースを開いた際に自動的にリソースを開放するように開放用の関数を指定したりしますね。
recoverを呼び出すと、巻き戻しを停止し、panicに渡された引数が返ります。巻き戻り中に実行できるコードは、deferで遅延指定された関数内のみなので、recoverは遅延指定された関数内でのみ役立ちます。
無敵かと思われたpanicですが、panicを起こしてもdeferステートメントに遅延指定した関数は呼ばれます。recover関数は遅延指定された関数内で呼び出すことで利用可能となります。
package main
import "fmt"
func main() {
fmt.Println("main start")
nansuPanic() // リカバーされるのでプログラムは終了せず
fmt.Println("main end") // 呼ばれる!
}
func nansuPanic() {
fmt.Println("nansuPanic start")
defer func() {
if err := recover(); err != nil {
fmt.Println("recover:", err) // panic関数の引数に指定されたナンナンゼミが出力される
}
}()
panic("(*>△<)<ナーンナーンっっ")
fmt.Println("nansuPanic end") // 呼ばれない
}
main start
nansuPanic start
recover: (*>△<)<ナーンナーンっっ
main end
とはいえ、重ねて言いますがpanicをいかに発生させないかが重要になってきます。
panicが発生しなければrecoverを行う必要がなくなってきます。
ただし、一部のライブラリでは正当な理由があり、使用している箇所があるようです。
→ panicはともかくrecoverに使いどころはほとんどない
ネストが深くなりすぎてエラーを戻すのが大変な処理みたいな時に使われているみたいですね。