🧭

Go 1.25以降でのslogについて

に公開

Go言語のプログラムで、動作の記録やエラーの原因調査に欠かせない「ログ」。Go 1.21で標準ライブラリに導入された構造化ログ機能log/slogが、Go 1.25でさらにアップデートされ、これまで少し手間がかかった点が解消され、より便利になりました。

主な変更点は以下の2つです。

1. 複数の情報をまとめて記録しやすくなった (slog.GroupAttrs)

ログには、複数の関連する情報(例: ユーザーIDとユーザー名)を一つのグループにまとめて表示したいことがよくあります。

これまで(Go 1.24まで)

プログラムの実行中に数が変わるような複数の情報をまとめてグループ化する場合、動作はするものの、少し遠回しで分かりにくい書き方が必要でした。

// Go 1.24までの実装例
attrs := []slog.Attr{slog.String("id", "123"), slog.String("name", "Alice")}

// 方法1: slog.Any()を使う → 動作するが直感的でない
slog.Info("ユーザー情報", slog.Any("user", attrs))
// 出力: level=INFO msg="ユーザー情報" user.id=123 user.name=Alice
// → 動作するが、[]slog.Attrをslog.Anyに渡すのは直感的でない

// 方法2: slog.Group()を使う → 動的な属性には使いにくい
slog.Info("ユーザー情報", 
    slog.Group("user",
        slog.String("id", "123"),
        slog.String("name", "Alice"),
    ))
// 出力: level=INFO msg="ユーザー情報" user.id=123 user.name=Alice
// → 正しい出力だが、属性が動的に決まる場合は書きにくい

// 方法3: 手動でGroupValueを作成 → 複雑
groupValue := slog.GroupValue(attrs...)
slog.Info("ユーザー情報", slog.Attr{Key: "user", Value: groupValue})
// 出力: level=INFO msg="ユーザー情報" user.id=123 user.name=Alice
// → 正しく動作するが、コードが複雑で直感的でない

https://go.dev/play/p/hRRAAh1hIBs?v=goprev

これから (Go 1.25)

slog.GroupAttrsという新しい機能が追加され、複数の情報をリストのまま直接グループにできるようになりました。

// Go 1.25のシンプルな書き方
attrs := []slog.Attr{slog.String("id", "123"), slog.String("name", "Alice")}

// GroupAttrs を使って、やりたいことを直接的に書ける
slog.Info("ユーザー情報", slog.GroupAttrs("user", attrs...))
// 出力: level=INFO msg="ユーザー情報" user.id=123 user.name=Alice
// → 意図通りにグループ化される

https://go.dev/play/p/xN96W8_WQbE

何がいいのか?

Go1.24でも動的なグループ化は可能でしたが、コードの意図がより明確で読みやすくなりました。slog.GroupAttrsという名前から、「複数の属性をグループ化する」という目的が一目で分かります。ログに出力したい情報を動的に組み立てるような場面で、コードの保守性が向上します。

2. ログがどこで出力されたか分かるようになった (Record.Source())

プログラムのどこでそのログが出力されたのか(ファイル名や行番号)は、問題解決の重要な手がかりになりますよね!

これまで

ログが出力された場所の情報はslogの内部には記録されていましたが、私たちがプログラムからその情報を取り出して利用することはできませんでした。
そのため、「特定のファイルから出たエラーログだけ色を変えて表示する」といった、ログの場所に応じた細かい処理を自作するのが困難でした。

これから (Go 1.25)

slog.Recordというログ情報の塊にSource()というメソッドが追加され、ファイル名や行番号を簡単に取得できるようになりました。

// ログの場所を使って処理をカスタマイズする例
func (h *MyHandler) Handle(ctx context.Context, r slog.Record) error {
    // r.Source() でログの発生場所を取得
    source := r.Source()
    if source != nil {
        // ファイル名と行番号が使える!
        fmt.Printf("ログの場所: %s:%d\n", source.File, source.Line)
        // 出力例: ログの場所: main.go:42
    }
    // ...
    return nil
}

https://go.dev/play/p/2bBpbHjCbyZ

何がいいのか?

ログが出力された場所に応じて、出力を変えたり、通知を送ったりといった柔軟な処理が可能になります。これにより、エラーの原因調査が格段にしやすくなります。

導入するには?

これらの新機能を使うのは簡単です。

  1. slog.GroupAttrsは、特別な設定なしですぐに使えます。
  2. Record.Source()でファイル名や行番号を取得したい場合は、slogを初期化する際にAddSource: trueという設定を追加する必要があります(パフォーマンス考慮のため、デフォルトでは無効)。
import "log/slog"
import "os"

func main() {
    // このオプションを追加するだけ
    opts := &slog.HandlerOptions{
        AddSource: true,
    }
    logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))

    logger.Info("このログにはファイル名と行番号が含まれます")
}

https://go.dev/play/p/G_OE_F7PxBT

まとめ

Go 1.25のslogは、この2つの改善によって、より直感的で柔軟なログ処理が可能になりました。日々の開発におけるログの管理や問題解決が、スムーズになりそうですね💪

株式会社ドクターズプライム

Discussion