Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Goでスタックトレースを扱う方法がややこしい件について

syumai
March 26, 2023

 Goでスタックトレースを扱う方法がややこしい件について

syumai

March 26, 2023
Tweet

More Decks by syumai

Other Decks in Programming

Transcript

  1. Go
    でスタックトレースを扱う方法がやや
    こしい件について
    syumai
    Kyoto.go remote #40 LT
    会 @cluster

    View Slide

  2. 自己紹介
    syumai
    Go Documentation
    輪読会 / ECMAScript
    仕様輪
    読会 主催
    Go
    でGraphQL
    サーバー (gqlgen)
    や TypeScript

    フロントエンドを書いています
    Twitter: @__syumai
    Website: https://syum.ai

    View Slide

  3. 目次
    スタックトレースとは
    Go
    のエラーにはスタックトレースが無い
    スタックトレースを簡単に使う方法2023

    自力でスタックトレースを取る方法

    View Slide

  4. 注意事項
    まだ結構調べてる最中なので、間違ってる情報もあるかもしれないで
    す!
    間違ったこと言ってたら教えてください!

    View Slide

  5. スタックトレースとは

    View Slide

  6. View Slide

  7. 概念整理
    スタックトレース
    プログラムの実行中の特定の時点でのコールスタックの状態をトレ
    ースしたもの
    コールスタック
    プログラムが実行中のサブルーチン (Go
    では関数?)
    に関する情報を
    記録するスタック
    サブルーチンが別のサブルーチンを呼んだら、それがスタックに積
    まれていく

    View Slide

  8. コールスタック
    多分こんな感じになってそう
    func main() {
    A()
    }
    func A() {
    B()
    }
    func B() {
    // Callstack: B(), A(), main()
    }

    View Slide

  9. なぜスタックトレースが欲しいのか
    エラーが発生した時に、 そのエラーがどこで生まれた物なのか 知り
    たい
    これが無いと、調査の時にハチャメチャに困ってしまう

    View Slide

  10. だがしかし

    View Slide

  11. Go
    のエラーにはスタックトレースが無い

    View Slide


  12. func main() {
    if err := A(); err != nil {
    fmt.Printf("%v", err) // error!
    }
    }
    func A() error {
    return B()
    }
    func B() error {
    return errors.New("error!")
    }

    View Slide

  13. 文言が一緒だと、誰が返したerror
    かわからない問題
    func main() {
    if err := A(); err != nil {
    fmt.Printf("%v", err) // error!
    }
    }
    func A() error {
    _ = B() //
    握り潰す
    return C() //
    返ってるのは C()
    の返したerror
    }
    func B() error {
    return errors.New("error!")
    }
    func C() error {
    return errors.New("error!")
    }

    View Slide

  14. スタックトレースを簡単に使う方法2023

    View Slide

  15. この辺のライブラリを使う
    morikuni/failure
    個人的にイチオシ (
    と言うか基本これしか使っていない)
    エラーコードベースのハンドリングは初め慣れないかもしれない
    慣れると最高
    cockroachdb/errors
    使ってないけどわりとよさそう
    ちゃんとメンテされてる &
    機能が豊富

    View Slide

  16. この辺のライブラリを使う
    pkg/errors
    2021
    年にArchive
    されたが未だ現役のところも多そう
    golang.org/x/xerrors
    一応使える
    xerrors.Errorf()
    がmulti-error
    対応してないので、本家のerror
    と差
    分が生まれてしまったのが渋い

    View Slide

  17. error
    周りのライブラリにどれを使うか?と言う歴史話は、そなたさんの
    ツイートに詳しく書いてあります
    https://twitter.com/sonatard/status/1639510779992113154?s=20

    View Slide

  18. ライブラリの使い方イメージ

    View Slide

  19. pkg/errors
    の例
    func A() error {
    return B()
    }
    func B() error {
    return pkg_errors.New("error happened")
    }
    func main() {
    err := A()
    if err != nil {
    // error happened
    fmt.Printf("%v\n", err)
    // error happened
    // main.B
    // /Users/syumai/go/src/github.com/syumai/til/go/errors/pkg_errors_example/basic/main.go:14
    // main.A
    // /Users/syumai/go/src/github.com/syumai/til/go/errors/pkg_errors_example/basic/main.go:10
    // main.main
    // /Users/syumai/go/src/github.com/syumai/til/go/errors/pkg_errors_example/basic/main.go:18
    // runtime.main
    // /opt/homebrew/Cellar/go/1.19.5/libexec/src/runtime/proc.go:250
    // runtime.goexit
    // /opt/homebrew/Cellar/go/1.19.5/libexec/src/runtime/asm_arm64.s:1172
    fmt.Printf("%+v\n", err)
    }
    }

    View Slide

  20. 自力でスタックトレースを取る方法

    View Slide

  21. 自力でスタックトレースを取る方法
    初めに書いた通り、スタックトレースは コールスタック の内容をトレ
    ースしたもの
    なので、やるのはコールスタックの情報を取得すること

    View Slide

  22. この辺の機能を使う
    runtime/debug.Stack() / runtime/debug.PrintStack()
    runtime.Callers() + runtime.CallersFrames()

    View Slide

  23. debug.Stack() / debug.PrintStack()
    debug.Stack()
    は、スタックトレースを文字列化したもののバイト列を
    返す
    本当に文字列でしかないので、データ形式としての汎用性は無い
    debug.PrintStack()
    はそれを直接Print
    してくれるだけ

    View Slide

  24. debug.Stack() / debug.PrintStack()
    func main() {
    A()
    }
    func A() {
    if err := B(); err != nil {
    debug.PrintStack()
    // goroutine 1 [running]:
    // runtime/debug.Stack()
    // /usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x65
    // runtime/debug.PrintStack()
    // /usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x19
    // main.A(...)
    // /tmp/sandbox4059372707/prog.go:14
    // main.main()
    // /tmp/sandbox4059372707/prog.go:9 +0x18
    }
    }
    func B() error {
    return errors.New("error!")
    }
    // https://go.dev/play/p/wSqAL9AOviY

    View Slide

  25. runtime.Callers() + runtime.CallersFrames()
    runtime.Callers()
    は、関数呼び出し時点でのコールスタック内のフレ
    ームのプログラムカウンタを返してくれる
    これを、runtime.CallersFrames()
    関数に渡すと、フレームの情報を
    取得できる
    フレームには、下記の情報が入っている
    関数名、ファイル名、行数
    (
    関数自体への参照も入っているらしいが、インライン化されてる
    可能性もあって、取れたり取れなかったりするらしい)

    View Slide

  26. runtime.Callers() / runtime.CallersFrames()
    で書いたprintStack
    関数
    func printStack() {
    var pc [100]uintptr
    n := runtime.Callers(0, pc[:])
    frames := runtime.CallersFrames(pc[:n])
    var (
    fr runtime.Frame
    ok bool
    )
    if _, ok = frames.Next(); !ok {
    return
    }
    for ok {
    fr, ok = frames.Next()
    if !ok {
    return
    }
    fmt.Println(fr.Function, fr.File, fr.Line)
    }
    }
    // https://go.dev/play/p/DkGku2xlhSr

    View Slide

  27. 自前のprintStack()
    関数を使った様子
    func main() {
    A()
    }
    func A() {
    if err := B(); err != nil {
    printStack()
    // main.printStack /tmp/sandbox3328056694/prog.go 25
    // main.A /tmp/sandbox3328056694/prog.go 15
    // main.main /tmp/sandbox3328056694/prog.go 10
    // runtime.main /usr/local/go-faketime/src/runtime/proc.go 250
    }
    }
    func B() error {
    return errors.New("error!")
    }
    // https://go.dev/play/p/DkGku2xlhSr

    View Slide

  28. まとめ
    Go
    のエラーにはスタックトレースが無い
    スタックトレースを使いたかったら、ライブラリを使う
    どのライブラリを使うべきか?と言う話は混迷している
    自力でスタックトレースを出力するには、runtime package
    を使う

    View Slide