使い捨てのコードのエラー処理について

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