スタック追跡とパニック・ハンドリング
今回は軽めの小ネタで。
「エラー・ハンドリングについて」でも少し説明したが, Go 言語では回復不能のエラー(ゼロ除算やメモリ不足など)が発生した場合には panic を投げる仕様になっている。 たとえば以下のコードでは
package main
import (
"fmt"
"os"
)
func main() {
os.Exit(run())
}
func run() int {
f()
return 0
}
func f() {
numbers := []int{0, 1, 2}
fmt.Println(numbers[3]) //panic!
}
以下のスタック情報が標準エラー出力に表示される1。 (The Go Playground での実行結果)
panic: runtime error: index out of range
goroutine 1 [running]:
main.f()
/tmp/sandbox269685094/main.go:19 +0x160
main.run(0x20300, 0x104000e0)
/tmp/sandbox269685094/main.go:13 +0x20
main.main()
/tmp/sandbox269685094/main.go:9 +0x20
まぁ必要な情報はあるのでこれでも構わないのだが,ファイル名がフルパスで表示されるのがアレな感じである。 また出力先が標準エラー出力で固定されているのも面白くない。
そこで panic 時の出力をカスタマイズすることを考える。
スタック情報を取得するには, panic を recover で捕まえた上で runtime
.Caller()
関数を使う。
package main
import (
"fmt"
"io"
"os"
"runtime"
)
func main() {
os.Exit(run(os.Stderr))
}
func run(log io.Writer) (exit int) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(log, "Panic: %v\n", r)
for depth := 0; ; depth++ {
pc, src, line, ok := runtime.Caller(depth)
if !ok {
break
}
fmt.Fprintf(log, " -> %d: %s: %s(%d)\n", depth, runtime.FuncForPC(pc).Name(), src, line)
}
exit = 1
}
}()
f()
exit = 0
return
}
func f() {
numbers := []int{0, 1, 2}
fmt.Println(numbers[3]) //panic!
}
これで出力は
Panic: runtime error: index out of range
-> 0: main.run.func1: /tmp/sandbox562252505/main.go(19)
-> 1: runtime.call16: /usr/local/go/src/runtime/asm_amd64p32.s(390)
-> 2: runtime.gopanic: /usr/local/go/src/runtime/panic.go(423)
-> 3: runtime.panicindex: /usr/local/go/src/runtime/panic.go(12)
-> 4: main.f: /tmp/sandbox562252505/main.go(36)
-> 5: main.run: /tmp/sandbox562252505/main.go(29)
-> 6: main.main: /tmp/sandbox562252505/main.go(11)
-> 7: runtime.main: /usr/local/go/src/runtime/proc.go(111)
-> 8: runtime.goexit: /usr/local/go/src/runtime/asm_amd64p32.s(1133)
となる。 ファイル名を出力したくないなら for 文の中を
for depth := 0; ; depth++ {
pc, _, line, ok := runtime.Caller(depth)
if !ok {
break
}
fmt.Fprintf(log, " -> %d: %s: (%d)\n", depth, runtime.FuncForPC(pc).Name(), line)
}
とする手もある。 コードを書いてる人はスタック追跡情報とファイルの行番号があれば大体あたりをつけられるので,これだけでもありがたい。
ブックマーク
- Goでfunctionが実行された順番を追いかける - sgykfjsm.github.com
- Go のバイナリには -ldflags ‘-w -s’ でコンパイルしてもたくさんパスが埋め込まれていた - Qiita
-
ちなみにこの情報は
-s
のリンクオプション(ビルド時に-ldflags "-s"
と指定する)でデバッグ用のシンボル情報を削除しても表示されるようだ。 ↩︎