
はじめに
こんにちは。製造エネルギー事業部の後藤です。
Go 1.25 Release Notes の内容を紹介する「フューチャー技術ブログ Go 1.25 リリース連載」記事です。encoding/json/v2パッケージを取り上げます。
Go 1.25では、新しいJSON実装である「encoding/json/v2
」が導入されました。これは、長らく使われてきたencoding/json
パッケージの将来的な刷新を見据えたものです。現時点では従来のencoding/json
が標準として使われており、v2
は実験的な位置づけですが、今後の開発やフィードバックを経て、標準パッケージとして採用される可能性があります。
encoding/json/v2 の要点
GOEXPERIMENT=jsonv2
を有効にすると、既存のencoding/json
パッケージの内部実装が新しいものに置き換わる- 新しいパッケージが2つ追加
encoding/json/v2
: 従来のencoding/json
を刷新した、高レベルなJSON処理パッケージencoding/json/jsontext
: 低レベルでJSON構文を扱うための基盤パッケージ
- 基本的な挙動(Marshal/Unmarshal)は変わらないため、多くのコードはそのまま動作。エラーメッセージは変わる可能性があり、新しいオプションも追加
- 性能向上し、特にJSONからGoの構造体へのUnmarshalが大幅に高速化。Unmarshalも既存実装と同等かそれ以上の性能
GOEXPERIMENT=jsonv2
でomitempty
の挙動は変わらないのか?
Go 1.25 のリリースノートには、GOEXPERIMENT=jsonv2
を有効にすると、既存の encoding/json
パッケージが新しい実装に切り替わるものの「Marshal/Unmarshal の基本的な挙動は変わらない」と明記されています。
「omitempty
タグもそのまま機能するなら、既存コードへの影響は少ないはず」と思い以下のコードを使って確認しました。
package main |
検証結果:変わりなし
このコードを Go 1.25 開発版環境で実行すると、以下の出力が得られます。
// --- 1. GOEXPERIMENT=jsonv2 なしの場合 --- |
既存のencoding/json
を使っているプロジェクトは、コードを大きく変更することなく、将来的に GOEXPERIMENT=jsonv2
の性能向上の恩恵を受けられそうです。
encoding/jsonとencoding/json/v2のomitemptyの挙動は違いあり
次にencoding/json/v2
でも同じように検証してみました。
先ほどのコード下記のように変更します。
--- main.go |
※Marshalを利用しあとからアウトプットのフォーマットを整えました
検証結果:違いあり
{ |
encoding/json と encoding/json/v2 のゼロ値の扱い
種別 | omitempty なし |
omitempty あり |
||
---|---|---|---|---|
- | encoding/json |
encoding/json/v2 |
encoding/json |
encoding/json/v2 |
文字列のゼロ値 ("" ) |
"" |
"" |
- | - |
数字のゼロ値 (0 ) |
0 | 0 | - | 0 |
boolのゼロ値 (false ) |
false |
false |
- | false |
ポインタがnil |
null |
null |
- | - |
スライス/マップがnil |
null |
[] / {} |
- | - |
スライス/マップが空値([] /{} ) |
[] / {} |
[] / {} |
- | - |
encoding/json/v2 の omitemptyの挙動が変わった理由(#71497)
挙動変更の背景にある議論の中心は、GoのIssue#71497
です。このIssueで議論された変更理由を要約します。
omitemptyの課題とv2での変更の意図
v1
の omitempty
オプションは、「フィールドがGoのゼロ値(false
、0
、nil
ポインタ、nil
インターフェース値、または空の配列、スライス、マップ、文字列)である場合にフィールドを省略する」と定義されていました。便利さの反面以下のような課題がありました。
omitempty
の省略ルールが、JSONの型システムではなくGoの型システムに密接に結びついている- JSONの文脈で「数値の
0
」と「値が存在しないnull
」を区別したい場合や、「空の配列[]
」と「存在しないことを示すnull
」を区別したい場合に、omitempty
が強制的に省略してしまう
v2
ではomitempty
の定義をよりJSONの型システムに沿ったものへと変更しています。そのためv2
のomitempty
は、v1
とは異なります。
v2
でv1
のomitempty
が提供していた「あらゆるゼロ値を省略する」という機能を実現するためには、jsonv2.OmitZeroStructFields(true)
を利用します。
性能検証
encoding/json/v2
の導入目的の一つは、性能向上にあります。特にUnmarshal(JSONからGoの構造体への変換)において顕著な速度差が見られるとのことなので検証してみました。
Goの標準ベンチマーク機能を使って、encoding/json
とencoding/json/v2
のMarshal/Unmarshalの性能を比較しました。
検証コード:ベンチマークテスト
type BenchmarkData struct { |
package main |
package main |
ベンチマーク結果
Goのベンチマークは、ns/op
(ナノ秒/操作)、B/op
(バイト/操作)、allocs/op
(アロケーション回数/操作)の3つの主要な指標で性能を示します。それぞれ、処理時間、メモリ使用量、メモリ確保回数を表し、値が低いほど高性能・高効率です。
ベンチマーク名 | ns/op (処理時間) | B/op (メモリ使用量) | allocs/op (メモリ確保回数) |
---|---|---|---|
Unmarshal_V1 | 4315 | 512 | 26 |
Unmarshal_V1 (GOEXPERIMENT=jsonv2あり) | 2161 | 112 | 8 |
Unmarshal_V2 | 2294 | 368 | 10 |
Marshal_V1 | 1166 | 672 | 11 |
Marshal_V1 (GOEXPERIMENT=jsonv2あり) | 1489 | 576 | 9 |
Marshal_V2 | 1387 | 512 | 5 |
Unmarshalは大きく性能向上
v1
(4315 ns/op
) と比較して、GOEXPERIMENT=jsonv2
を付けた v1
(2161 ns/op
) は約2倍の高速化しています。メモリ使用量とアロケーション回数も大きく削減されています。encoding/json/v2
を直接使用した場合も2294 ns/op
と同様に高速です。
Marshalも性能向上
処理時間自体は v1
(1166 ns/op
) に対して v1
(GOEXPERIMENT=jsonv2
あり) (1489 ns/op
) および v2
(1387 ns/op
) はわずかに遅いか同等ですが、メモリ使用量とアロケーション回数は全てのv2
関連のケースで向上しています。
GOEXPERIMENT=jsonv2
を用いることで既存のコードはそのままで、特にUnmarshalで大きな性能・リソース効率改善が見られました。比較的小さなデータでもかなり性能が上がっていたので嬉しい限りです。
その他の重要な変更点と設計思想
- 構文と意味の分離(
jsontext
の導入)v2
は、JSON の構文解析のみを行う低レベルなencoding/json/jsontext
パッケージを基盤としている。Goのリフレクションに依存しない高速な構文処理と型への変換(セマンティック処理)が明確に分離されたjsonv2.NewEncoder
などのストリーミングAPIを支える基盤
- ストリーミングAPIの強化:
jsonv2.NewEncoder
やjsonv2.NewDecoder
を通じて、メモリ効率の良いストリーミングでのMarshal/Unmarshalが可能に。io.Writer
やio.Reader
へ直接JSONの読み書きで、大きなJSONデータでも性能改善が期待される
まとめ
encoding/json/v2
の紹介と特に混乱しやすいomitempty
タグの挙動と性能に焦点を当て、深掘りしました。encoding/json/v2
を理解し活用するための一助となれば幸いです。