使い捨てのコードのエラー処理について
tl;dr
- panic時ではなくerror時にもfullのstack traceが欲しい
- pkg/errors が便利
はじめに
しばらくgoを書いていて、使い捨てのコードのエラー処理についてどうすれば良いのか考えたりしていた。ここで言う使い捨てのコードというのは1ファイル位で作れそうな小さなコマンドラインのコマンドのようなものを指している。
まともなアプリケーションコードでは考えることが色々ある気がするけれど。使い捨てのコードなら以下を満たしていれば十分だと思った。
- 終了ステータスが0以外になる
- エラーの発生箇所が正確に分かる(stack trace)
前者はテキトーに書いても自然に満たす気がする。ここでは後者をどうするかについて書く。
panic時は問題なし。ただしerror時には問題がある
ここでいうエラー処理は以下の2つを含んでいる。
- panic時の処理
- error時の処理
テキトーに書いた場合にどういう状況か整理。
panic時の処理
panic()
により中断された場合にはstack traceを出力してくれる。なのでpanic時は何もしなくても期待通りのstack traceが出力される。
package main func foo() { panic("hmm") } func main(){ foo() }
main()
の中で foo()
を呼び foo()
の内部でpanicが起きたということが分かる。
panic: hmm goroutine 1 [running]: panic(0x56d40, 0xc82000a0c0) /opt/local/lib/go/src/runtime/panic.go:481 +0x3e6 main.foo() /home/podhmo/go-sandbox/examples-errors/example_stacktrace/00panic.go:4 +0x65 main.main() /home/podhmo/go-sandbox/examples-errors/example_stacktrace/00panic.go:8 +0x14 exit status 2
error時の処理
errorの時はどうなるか整理してみる。通常は、内部の関数はerror値を含んだ値を返し、トップレベルでerror値をまじめに取り扱うという感じになると思う。
package main import ( "fmt" ) func foo() error { return fmt.Errorf("hmm") } func main() { err := foo() if err != nil { panic(err) } }
foo()
の処理自体はerror値を返すという形で正常に行われている。そのため、当然ではあるけれど、error message自体は元のエラーのものが表示されるものの、stack traceは元のエラーの発生箇所ではなくトップレベルのものになってしまう。
panic: hmm goroutine 1 [running]: panic(0xd5f00, 0xc82000a120) /opt/local/lib/go/src/runtime/panic.go:481 +0x3e6 main.main() /home/podhmo/go-sandbox/examples-errors/example_stacktrace/01error.go:15 +0x59 exit status 2
エラー発生箇所のstack traceを保持したままにしたい
エラー発生箇所のstack traceを保持したままerror値を伝搬させていきたい。 これは pkg/errors を使うとできそう。
+フラグ付きで出力するとstack traceも含めてくれる。
package main import ( "fmt" "github.com/pkg/errors" "os" ) func foo() error { return errors.Errorf("hmm") } func main() { err := foo() if err != nil { fmt.Printf("error: %+v\n", err) os.Exit(1) } }
以下の様な感じ。今回は foo()
の内部でエラーが発生していることが分かる。
error: hmm main.foo /home/podhmo/go-sandbox/examples-errors/example_stacktrace/02error.go:10 main.main /home/podhmo/go-sandbox/examples-errors/example_stacktrace/02error.go:14 runtime.main /opt/local/lib/go/src/runtime/proc.go:188 runtime.goexit /opt/local/lib/go/src/runtime/asm_amd64.s:1998 exit status 1
appendix
もう少しだけ pkg/errors
のことを詳しく。基本的には以下の様にすれば良い。
- 自分でエラーを発生させる ->
fmt.Errorf()
のかわりにerrors.Errorf()
を使う - 内部の関数で発生したエラーを伝搬させる -> 直接error値を返すより、
errors.Wrapf()
でwrapした値を返す
package main import ( "github.com/pkg/errors" "fmt" "os" ) func f0() error{ err := f1() if err != nil { return errors.Wrapf(err, "f0") } return err } func f1() error{ err := f2() if err != nil { return errors.Wrapf(err, "f1") } return err } func f2() error{ err := f3() if err != nil { return errors.Wrapf(err, "f2") } return err } // 外部のパッケージでのエラー func f3() error{ return fmt.Errorf("*error on a external package*") } func main() { err := f0() if err != nil { fmt.Printf("err %+v\n", err) os.Exit(1) } }
ちょっと出力が冗長ではあるけれど。完全なstack traceが取れる。
err *error on a external package* f2 main.f2 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:26 main.f1 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:17 main.f0 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:10 main.main /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:35 runtime.main /opt/local/lib/go/src/runtime/proc.go:188 runtime.goexit /opt/local/lib/go/src/runtime/asm_amd64.s:1998 f1 main.f1 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:19 main.f0 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:10 main.main /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:35 runtime.main /opt/local/lib/go/src/runtime/proc.go:188 runtime.goexit /opt/local/lib/go/src/runtime/asm_amd64.s:1998 f0 main.f0 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:12 main.main /home/podhmo/go-sandbox/examples-errors/example_stacktrace/03nested.go:35 runtime.main /opt/local/lib/go/src/runtime/proc.go:188 runtime.goexit /opt/local/lib/go/src/runtime/asm_amd64.s:1998 exit status 1
直接 stack trace的な情報を取り出す
あと、stack trace的な情報だけ欲しい場合には以下の様にすれば良さそう。
func main() { type causer interface { Cause() error } type stackTracer interface { StackTrace() errors.StackTrace } err := f0() if err != nil { errs := []stackTracer{} for err != nil { if err, ok := err.(stackTracer); ok { errs = append(errs, err) } if cause, ok := err.(causer); !ok { break } err = cause.Cause() } fmt.Println("stack trace") for _, frame := range errs[len(errs)-1].StackTrace() { fmt.Printf("\t %+v\n", frame) } os.Exit(1) } }
出力結果
stack trace main.f2 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/04stacktracer.go:26 main.f1 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/04stacktracer.go:17 main.f0 /home/podhmo/go-sandbox/examples-errors/example_stacktrace/04stacktracer.go:10 main.main /home/podhmo/go-sandbox/examples-errors/example_stacktrace/04stacktracer.go:42 runtime.main /opt/local/lib/go/src/runtime/proc.go:188 runtime.goexit /opt/local/lib/go/src/runtime/asm_amd64.s:1998 exit status 1
golangのfmt系のformatの機能のメモ
まじめに調べるなら以下を見たほうが良い。
reflection使った便利な出力
%T
値の方を表示%v
値を良い感じに表示%+v
+フラグ付きで冗長出力表示。%#v
値を型名やフィールド名も含めて出力
利用例
type Person struct { Name string Age int } func main(){ person := Person{Name: "foo", Age: 20} fmt.Printf("%%T %T\n", person) fmt.Printf("%%v %v\n", person) fmt.Printf("%%v %#v\n", person) } /* %T main.Person %v {foo 20} %v main.Person{Name:"foo", Age:20} */
同一の値を添え字で参照
1-originなことに注意
fmt.Printf("type=%[1]T, value=%[1]v, verbose=%#[1]v\n", person) /* type=main.Person, value={foo 20}, verbose=main.Person{Name:"foo", Age:20} */
quoteされた文字列の表示
%q
が用意されている。
fmt.Printf("string = %q¥n", "foo") /* string = "foo" */
0-padding
0-paddingだけできる?
fmt.Printf("long=%06d, short=%04[1]d\n", 100) /* long=000100, short=0100 */