Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介 syumai Go Documentation 輪読会 / ECMAScript 仕様輪 読会 主催 Go でGraphQL サーバー (gqlgen) や TypeScript で フロントエンドを書いています Twitter: @__syumai Website: https://syum.ai

Slide 3

Slide 3 text

目次 スタックトレースとは Go のエラーにはスタックトレースが無い スタックトレースを簡単に使う方法2023 春 自力でスタックトレースを取る方法

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

スタックトレースとは

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

だがしかし

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

文言が一緒だと、誰が返した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!") }

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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) } }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

自前の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

Slide 28

Slide 28 text

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