- 後藤玲雄
- 約 3,300 文字
- 2,200 View
目次
はじめに
こんにちは。製造エネルギー事業部の後藤です。
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の導入目的の1つは、性能向上にあります。特に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を理解し活用するための一助となれば幸いです。