Goプログラムの実行トレースを出力するxtracegoを開発した

はじめに

こんにちは,Jumpakuです!本記事はGo Advent Calendar 2025 Series 2の22日目の記事です.

qiita.com

本記事では,Goの自作ツール「xtracego」を開発した話を紹介します.

github.com

xtracegoとはどんなツールなのか

xtracegoはgoプログラムを実行トレース付きで実行するCLIアプリケーションです. 実行トレースとは,goプログラムの実行状況のリアルタイムなログのことで,以下はxtracegoによる出力例です.

// fizzbuzz/main.go
package main

import "fmt"

const N = 20

func main() {
    for i := 1; i <= N; i++ {
        if i%15 == 0 {
            fmt.Println("FizzBuzz")
        } else if i%3 == 0 {
            fmt.Println("Fizz")
        } else if i%5 == 0 {
            fmt.Println("Buzz")
        } else {
            fmt.Println(i)
        }
    }
}
xtracego run ./fizzbuzz
2025-12-13T20:47:07Z [ 1] main.init: const N = 20 ------------------------------------ /path/to/examples/fizzbuzz/main.go:8:7
2025-12-13T20:47:07Z [ 1] main.init: [VAR] N=20
2025-12-13T20:47:07Z [ 1] main.main: [CALL] func main()
2025-12-13T20:47:07Z [ 1] main.main:     for i := 1; i <= N; i++ { ------------------ /path/to/examples/fizzbuzz/main.go:11:2
2025-12-13T20:47:07Z [ 1] main.main: [VAR] i=1
2025-12-13T20:47:07Z [ 1] main.main:         if i%15 == 0 { ------------------------- /path/to/examples/fizzbuzz/main.go:12:3
2025-12-13T20:47:07Z [ 1] main.main:         } else if i%3 == 0 { ------------------ /path/to/examples/fizzbuzz/main.go:14:10
2025-12-13T20:47:07Z [ 1] main.main:         } else if i%5 == 0 { ------------------ /path/to/examples/fizzbuzz/main.go:16:10
2025-12-13T20:47:07Z [ 1] main.main:         } else { ------------------------------- /path/to/examples/fizzbuzz/main.go:18:3
2025-12-13T20:47:07Z [ 1] main.main:             fmt.Println(i) --------------------- /path/to/examples/fizzbuzz/main.go:19:4
1

ここではGoプログラム fizzbuzz/main.go を xtracego run ./fizzbuzz と実行することで,変数値,関数呼び出し,実行ステートメントとそのソースファイル上の位置といった情報が出力されています.

どうしてxtracegoを作ったのか

シェルスクリプトでは,実行トレースを出力するためのコマンド set -x というものがあり,これを使えば手軽にデバッグやログ保存をすることができます. これが便利なので,ちょっとした自動化やバッチ処理シェルスクリプトで書くことが多いのですが,環境毎に微妙に互換性がなかったり,ちょっと複雑になるとチームで保守するのが大変になったり,静的解析が効かず書くのが面倒だったりして,実際にはあんまりシェルスクリプトを書きたくないと思っていました.

代わりに,「静的型付けで,標準ツール・ライブラリが充実していて,クロスコンパイルできて,文法もシンプルなGoで書きたい!」と思うことも何度もありましたが,その度に「でも実行トレースが取れないからなあ」と躊躇してしまっていました. 実際,実行トレースを取るためには,fmt.Println を差し込んでいかないといけませんが,これはものすごく面倒な作業です. 他にも,デバッガでステップ実行すれば,実行状況を追いながらデバッグすることができますが,ログ保存には使えません. また,スタックトレースを使えば,エラー箇所をログに残すことができますが,panicにならない不具合には利用できず,実行状況を追うこともできません.

「Goでも手軽にデバッグとログ保存がしたい!実行トレースを出力できるツールが欲しい!欲しいなら自分で作れば良いじゃないか!」そう思い至り,開発を始めたのでした.

xtracegoはどんな仕組みで動作するのか

xtracegoの動作原理を簡単に説明します.

xtracegoを実行すると,指定されたパッケージとその依存パッケージに含まれるソースファイルからそれぞれAST(Abstract Syntax Tree,抽象構文木)を構築し,各ステートメントに対して,そのログを出力するコードを自動的に注入します.こうして変更されたASTからソースファイルを生成し,実行ファイルをビルド,実行します.

例えば以下のようにログ出力コードが注入されたソースファイルが内部的に生成されるのですが,ここではステートメントfmt.Printf("Hello, Golang\n")の前にログ出力コードが差し込まれているのがわかると思います.

// オリジナルのソースファイル
package main

import "fmt"

func main() {
    fmt.Printf("Hello, Golang\n")
}
// ログ出力コードが注入されたソースファイル
package main

import (
    "fmt"
    "example/xtracego_xvlbzgba"
)

func main() {
    xtracego_xvlbzgba.PrintlnCall_xvlbzgba(200, "func main()", true, true)
    defer xtracego_xvlbzgba.PrintlnReturn_xvlbzgba(200, "func main()", true, true)
    xtracego_xvlbzgba.PrintlnStatement_xvlbzgba(3, 200, "    fmt.Printf(\"Hello, Golang\\n\") ", " /Users/jumpaku/example/hello/main.go:8:2", true, true)
    fmt.Printf("Hello, Golang\n")
}

このように,AST操作により実行トレースを実現しているので,構文的な正しさを保証できる実装となっています.

その他関連資料

「Goプログラムに実行ログを注入する「xtracego」の試作」

以下はGo Connect #10のLTで発表した資料で,試作したxtracegoのコンセプトをデモを交えて説明しました. drive.google.com

「実行トレースを出力する「xtracego」を実現するためのAST操作」

以下はgolang.tokyo #42のLTで発表した資料で,xtracegoの内部実装で利用しているAST操作について説明しました.

drive.google.com

おわりに

短い記事でしたが,自作ツール「xtracego」とその開発経緯について紹介させていただきました.興味を持っていただけた場合は,GitHubでのスターやプルリクエストなど大歓迎ですのでよろしくお願いいたします!