|
| 1 | +--- |
| 2 | +title: "Go 1.25 リリース連載 log/slog" |
| 3 | +date: 2025/07/31 00:00:00 |
| 4 | +postid: a |
| 5 | +tag: |
| 6 | + - Go |
| 7 | + - Go1.25 |
| 8 | + - 構造化ログ |
| 9 | + - ログ |
| 10 | +category: |
| 11 | + - Programming |
| 12 | +thumbnail: /images/20250731a/thumbnail.jpg |
| 13 | +author: 武田大輝 |
| 14 | +lede: "log/slog パッケージのアップデートについて紹介します。Go の log/slog パッケージは、Go 1.21 で導入された構造化ロギングをサポートする標準ライブラリです。" |
| 15 | +--- |
| 16 | + |
| 17 | +<img src="/images/20250731a/top.jpg" alt="" width="512" height="512"> |
| 18 | + |
| 19 | +# はじめに |
| 20 | + |
| 21 | +[Go 1.25 リリース連載](/articles/20250730a/) の 2 本目です。 |
| 22 | + |
| 23 | +本記事では [log/slog パッケージ](https://pkg.go.dev/log/slog@go1.25rc2) のアップデートについて紹介します。 |
| 24 | +Go の log/slog パッケージは、Go 1.21 で導入された構造化ロギングをサポートする標準ライブラリです。 |
| 25 | + |
| 26 | +本記事では slog についての基本的は割愛しますが、slog の概要やこれまでのアップデート経緯をつかみたい方は、過去のリリース連載記事を参照してください。 |
| 27 | + |
| 28 | +- [Go 1.21 連載始まります&slog をどう使うべきか](/articles/20230731a/) |
| 29 | +- [Go 1.22 リリース連載 vet, log/slog, testing/slogtest](/articles/20240205a/) |
| 30 | + |
| 31 | +# アップデートの概要 |
| 32 | + |
| 33 | +[リリースノート](https://go.dev/doc/go1.25#logslogpkglogslog) を参照してみましょう。 |
| 34 | + |
| 35 | +> GroupAttrs creates a group Attr from a slice of Attr values. |
| 36 | +> |
| 37 | +> Record now has a Source method, returning its source location or nil if unavailable. |
| 38 | +
|
| 39 | +- **`slog.GroupAttrs` の追加** |
| 40 | + 複数の属性 (`[]slog.Attr`) を簡潔にグルーピングできるようになりました。 |
| 41 | +- **`Record.Source()` の公開** |
| 42 | + ログエントリの発生元(ファイル・行番号・関数名)を取得できるようになりました。 |
| 43 | + |
| 44 | +それぞれ詳細な内容を見ていきましょう。 |
| 45 | + |
| 46 | +# アップデートの詳細 |
| 47 | + |
| 48 | +## slog.GroupAttrs による属性のグルーピング([#66365](https://github.com/golang/go/issues/66365)) |
| 49 | + |
| 50 | +`slog.GroupAttrs` を使用して構造化ログの属性(要素)をグルーピングできるようになりました。 |
| 51 | + |
| 52 | +```go |
| 53 | + g := slog.GroupAttrs("user", |
| 54 | + slog.String("id", "00001"), |
| 55 | + slog.String("name", "Bob"), |
| 56 | + ) |
| 57 | + // {"level":"INFO","msg":"GroupAttrs","user":{"id":"00001","name":"Bob"}} |
| 58 | + logger.Info("GroupAttrs", g) |
| 59 | +``` |
| 60 | + |
| 61 | +属性のグルーピング自体はもともと `slog.Group` を使用して実現できましたが、属性を動的に生成する場合にいくつか使用上の問題がありました。 |
| 62 | + |
| 63 | +### 属性が静的な場合 |
| 64 | + |
| 65 | +```go |
| 66 | + g := slog.Group("user", |
| 67 | + slog.String("id", "00001"), |
| 68 | + slog.String("name", "Bob"), |
| 69 | + ) |
| 70 | + // {"level":"INFO","msg":"Group","user":{"id":"00001","name":"Bob"}} |
| 71 | + logger.Info("Group", g) |
| 72 | +``` |
| 73 | + |
| 74 | +### 属性が動的な場合 |
| 75 | + |
| 76 | +条件に応じて属性を追加する場合などは `slog.Group` がうまく機能しません。 |
| 77 | + |
| 78 | +```go |
| 79 | + attrs := []slog.Attr{ |
| 80 | + slog.String("id", "00001"), |
| 81 | + slog.String("name", "Bob"), |
| 82 | + } |
| 83 | + |
| 84 | + // 条件に応じて属性を追加 |
| 85 | + if showAge { |
| 86 | + attrs = append(attrs, slog.Int("age", 36)) |
| 87 | + } |
| 88 | + |
| 89 | + // []slog.Attr doesn't match []any となり動かない |
| 90 | + g := slog.Group("user", attrs...) |
| 91 | +``` |
| 92 | + |
| 93 | +そのためこれまでは `slog.Any` を利用したり、`[]slog.Attr` を `[]any` に変換したりして対応してきた背景があります。 |
| 94 | + |
| 95 | +```go |
| 96 | + // slog.Any を使用する |
| 97 | + g := slog.Any("user", slog.GroupValue(attrs...)) |
| 98 | + |
| 99 | + // ヘルパーファンクションを用意して []any に変換する |
| 100 | + g := slog.Group("key", attr2any(attrs)...) |
| 101 | + |
| 102 | + // []any を使用する |
| 103 | + var attrs []any |
| 104 | + ... |
| 105 | + g := slog.Group("key", attrs...) |
| 106 | +``` |
| 107 | + |
| 108 | +このアプローチは any の使用が避けられず、適切なコードサジェストがなされないなど直感的ではないということで `[]slog.Attr` を引数として渡せる `slog.GroupAttrs` が生まれました。 |
| 109 | + |
| 110 | +## record.Source によるソース情報の取得([#70280](https://github.com/golang/go/issues/70280)) |
| 111 | + |
| 112 | +Go の log/slog では、1 件のログエントリを内部では `slog.Record` という構造体で表現しています。 |
| 113 | +Go1.25 で追加された `record.Source` はレコードからログの発生元(ファイル名・行番号・関数名)を返します。 |
| 114 | +ソースの [Diff](https://github.com/golang/go/commit/044ca4e5c878c785e2c69e5ebcb3d44bf97abc9f) を見るとはもともとパッケージ内限定(unexported)だったものが公開された(export)された形になります。 |
| 115 | + |
| 116 | +これにより、自作のカスタムハンドラなどからもログのソース情報にアクセスできます。 |
| 117 | +サンプルソースは次の通りです。 |
| 118 | + |
| 119 | +```go |
| 120 | +func main() { |
| 121 | + logger := slog.New(NewSourceHandler(slog.NewJSONHandler(os.Stdout, nil))) |
| 122 | + g := slog.Group("user", |
| 123 | + slog.String("id", "00001"), |
| 124 | + slog.String("name", "Bob"), |
| 125 | + ) |
| 126 | + // LOG from /xxx/main.go:61 (main) |
| 127 | + logger.Info("Group", g) |
| 128 | +} |
| 129 | + |
| 130 | + |
| 131 | +type SourceHandler struct { |
| 132 | + h slog.Handler |
| 133 | +} |
| 134 | + |
| 135 | +func NewSourceHandler(h slog.Handler) *SourceHandler { |
| 136 | + return &SourceHandler{ |
| 137 | + h: h, |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +func (sh SourceHandler) Enabled(ctx context.Context, level slog.Level) bool { |
| 142 | + return sh.h.Enabled(ctx, level) |
| 143 | +} |
| 144 | + |
| 145 | +func (sh SourceHandler) Handle(ctx context.Context, r slog.Record) error { |
| 146 | + // ソース情報を出力 |
| 147 | + if src := r.Source(); src != nil { |
| 148 | + fmt.Printf("LOG from %s:%d (%s)\n", src.File, src.Line, src.Function) |
| 149 | + } |
| 150 | + return sh.h.Handle(ctx, r) |
| 151 | +} |
| 152 | + |
| 153 | +func (sh SourceHandler) WithAttrs(attrs []slog.Attr) slog.Handler { |
| 154 | + return SourceHandler{h: sh.h.WithAttrs(attrs)} |
| 155 | +} |
| 156 | + |
| 157 | +func (sh SourceHandler) WithGroup(name string) slog.Handler { |
| 158 | + return SourceHandler{h: sh.h.WithGroup(name)} |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +# おわりに |
| 163 | + |
| 164 | +Go 1.25 の `log/slog` では、構造化ログの柔軟性と拡張性がさらに向上しました。 |
| 165 | + |
| 166 | +サンプルのソースコードは [こちらのリポジトリ](https://github.com/rhumie/tech-blog-go-1.25-feature) で公開しています。 |
| 167 | + |
| 168 | +次回は `sync` のアップデートについてです。 |
0 commit comments