本記事でやること
今回はGo1.25から実験的に導入された(今後標準パッケージとして採用される可能性がある)encoding/json/v2のUnmarshalRead関数とMarshalWrite関数を使った簡単な実装例をencoding/json (v1)と比較しながら紹介します。また、ベンチマーク機能によってUnmarshalRead関数とUnmarshal関数(v1)の性能比較をします。
今回実装したコードは以下のレポジトリで公開しています。
対象読者
-
encoding/json/v2
のUnmarshalRead
関数/MarshalWrite
関数が気になっている方
使用言語
- Go 1.25.0
UnmarshalRead/MarshalWrite関数の定義
encoding/json/v2にUnmarshalRead関数とMarshalWrite関数が追加されました。encondig/json (v1)のUnmarshal
関数とMarshal
関数と違い、io.Reader
/io.Writer
を受け取れるようになりました。
encoding/json/v2 | encoding/json |
---|---|
func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) | func Unmarshal(data []byte, v any) (err error) |
func MarshalWrite(out io.Writer, in any, opts ...Options) (err error) | Marshal(v any) ([]byte, error) |
io.Reader
/io.Writer
インターフェイスの実装を満たす構造体を受け取れるようになります。つまり、以下のような様々な型から直接データを読み書きできるようになり柔軟性が向上しました。
io.Readerを実装している型の例
os.File
bytes.Buffer
strings.Reader
http.Request.Body
以下のようにv2ではstrigs.Reader
型から直接読み込めるようになります。
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
var p Person
reader := strings.NewReader(`{"name":"taro","age":30}`)
_ := json.UnmarshalRead(reader, &p)
// v1の例
// data := []byte(`{"name":"taro","age":30}`)
// _ := json.Unmarshal(data, &p)
以下、UnmarshalRead
関数の説明にあるように入力は単一のJSONオブジェクトでなければなりません。また、EOFまで読み取り、EOFはエラーとして扱いません。
UnmarshalRead deserializes a Go value from an io.Reader according to the provided unmarshal and decode options (while ignoring marshal or encode options). The input must be a single JSON value with optional whitespace interspersed. It consumes the entirety of io.Reader until io.EOF is encountered, without reporting an error for EOF. The output must be a non-nil pointer. See Unmarshal for details about the conversion of JSON into a Go value.
io.Writerを実装している型の例
os.File
bytes.Buffer
http.ResponseWriter
以下のようにv2ではbytes.Buffer型へ直接書き込めるようになります。
buf := new(bytes.Buffer)
person := Person{Name: "hanako", Age: 25}
json.MarshalWrite(buf, person)
fmt.Printf("Marshaled Person (v2): %s\n", buf.String())
// Marshaled Person (v2): {"name":"hanako","age":25}
// v1の例
// person := Person{Name: "hanako", Age: 25}
// b, _ := json.Marshal(person)
// fmt.Printf("Marshaled Person (v1): %s\n", b)
// Marshaled Person (v1): {"name":"hanako","age":25}
実装例
ファイルの読み込みから構造体への格納
ファイルを読み込み、構造体に格納する処理をUnmarshalRead
関数を用いて実装をしました。
UnmarshalRead
関数により、直接ファイルからストリーミング読み込みが可能になります。v1のUnmarshal
関数を用いた場合と比較し、中間バッファを必要としないのでメモリ使用量の削減に繋がりパフォーマンスの向上が期待できます。
file, err := os.Open("./test_v2.json")
if err != nil {
panic(err)
}
defer file.Close()
var p Person
err = json.UnmarshalRead(file, &p)
if err != nil {
panic(err)
}
fmt.Printf("Loaded Person (v2): %+v\n", p)
// Loaded Person (v2): {Name:hanako Age:25}
v1(encoding/json
)のUnmarshal
関数を用いた処理を以下に記述します。
data, err := os.ReadFile("./test_v1.json")
if err != nil {
panic(err)
}
var p Person
err = json.Unmarshal(data, &p)
if err != nil {
panic(err)
}
fmt.Printf("Loaded Person (v1): %+v\n", p)
// Loaded Person (v1): {Name:hanako Age:25}
構造体からファイルへの書き込み
ファイルへの書き込み処理も同様にv2ではos.Create
関数から出力されたio.Writer
インターフェイスの実装を満たすos.File
構造体をそのまま受け取り直接ファイルへの書き込みが可能です。
file, err := os.Create("./test_v2.json")
if err != nil {
panic(err)
}
defer file.Close()
p := Person{Name: "hanako", Age: 25}
err = json.MarshalWrite(file, p)
if err != nil {
panic(err)
}
v1のMarshal
関数を用いた処理を以下に記述します。
p := Person{Name: "hanako", Age: 25}
data, err := json.Marshal(p)
if err != nil {
panic(err)
}
err = os.WriteFile("./test_v1.json", data, 0644)
if err != nil {
panic(err)
}
リクエストボディの読み込みから構造体への格納
APIなどリクエストボディとして、単一のJSONを受け取る際もUnmarshalRead関数を活用することができます。http.Request
構造体のBody
フィールドはio.ReadCloser
型です。io.ReadCloser
インターフェイスはio.Reader
, io.Writer
の複合的なインターフェイスなので、UnmarshalRead
関数はhttp.Request.Body
をそのまま受け取ることができます。
http.HandleFunc("/v2", func(w http.ResponseWriter, r *http.Request) {
var p Person
err := json.UnmarshalRead(r.Body, &p)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("Unmarshaled Person (v2): %+v\n", p)
})
v1ではリクエストボディをio.ReadAll
などで読み込み一度byte型に変換してからUnmarshal
関数へ入力する必要があります。
http.HandleFunc("/v1", func(w http.ResponseWriter, r *http.Request) {
var p Person
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = json.Unmarshal(body, &p)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("Unmarshaled Person (v1): %+v\n", p)
})
構造体からレスポンスの生成
http.ResponseWriter
型もio.Writer
の実装を満たしているため、そのままMarshalWriter
関数は受け取ることができます。
http.HandleFunc("/v2", func(w http.ResponseWriter, r *http.Request) {
var p Person
err := json.UnmarshalRead(r.Body, &p)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("Unmarshaled Person (v2): %+v\n", p)
res := Person{Name: "taro", Age: 30}
err = json.MarshalWrite(w, res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
http.HandleFunc("/v1", func(w http.ResponseWriter, r *http.Request) {
var p Person
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = json.Unmarshal(body, &p)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
fmt.Printf("Unmarshaled Person (v1): %+v\n", p)
res := Person{Name: "taro", Age: 30}
b, err := json.Marshal(res)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
ベンチマーク
encoding/json/v2
のUnmarshalRead
関数とencoding/json
のUnmarshal
関数の性能をベンチーマーク機能を使って比較します。
Go1.25でGOEXPERIMENT=jsonv2
を有効にするとencoding/json
のUnmarshal関数は内部的にv2のUnmarshal関数を呼ぶことになるので、ベンチマークのファイルを2つに分け、v1のUnmarshal関数を実行する際はGOEXPERIMENT=jsonv2
を有効にせず実行します。
import (
"encoding/json/v2"
"strings"
"testing"
)
type BenchPerson struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address string `json:"address"`
Phone string `json:"phone"`
Description string `json:"description"`
Tags []string `json:"tags"`
Metadata map[string]interface{} `json:"metadata"`
}
var (
mediumJSONString = `{
"name":"John Doe",
"age":30,
"email":"john@example.com",
"address":"123 Main St, City, Country",
"phone":"+1234567890",
"description":"A software developer with 10 years of experience",
"tags":["developer","golang","backend"],
"metadata":{"level":"senior","department":"engineering","team":"platform"}
}`
)
func BenchmarkUnmarshalReadV2_Medium(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
reader := strings.NewReader(mediumJSONString)
var p BenchPerson
_ = json.UnmarshalRead(reader, &p)
}
}
GOEXPERIMENT=jsonv2 go test -bench=. -benchmem benchmark_v2_test.go
goos: darwin
goarch: arm64
cpu: Apple M3 Pro
BenchmarkUnmarshalReadV2_Medium-12 998328 1245 ns/op 704 B/op 14 allocs/op
PASS
ok command-line-arguments 2.142s
import (
"encoding/json"
"testing"
)
type BenchPerson struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address string `json:"address"`
Phone string `json:"phone"`
Description string `json:"description"`
Tags []string `json:"tags"`
Metadata map[string]interface{} `json:"metadata"`
}
var (
mediumJSON = []byte(`{
"name":"John Doe",
"age":30,
"email":"john@example.com",
"address":"123 Main St, City, Country",
"phone":"+1234567890",
"description":"A software developer with 10 years of experience",
"tags":["developer","golang","backend"],
"metadata":{"level":"senior","department":"engineering","team":"platform"}
}`)
)
func BenchmarkUnmarshalV1_Medium(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var p BenchPerson
_ = json.Unmarshal(mediumJSON, &p)
}
}
go test -bench=. -benchmem benchmark_v1_test.go
goos: darwin
goarch: arm64
cpu: Apple M3 Pro
BenchmarkUnmarshalV1_Medium-12 386142 2646 ns/op 1120 B/op 32 allocs/op
PASS
ok command-line-arguments 1.351s
ベンチマーク結果
UnmarshalRead
関数は全ての指標で大幅な改善が達成されています。特に処理速度は2倍以上高速化しています。
指標 | UnmarshalRead関数(encoding/json/v2) | Unmarshal関数(encoding/json) |
---|---|---|
処理速度(ns/op) | 1245(ns/op) | 2646(ns/op) |
メモリ使用量(B/op) | 704(B/op) | 1120(B/op) |
アロケーション回数(allocs/op) | 14(allocs/op) | 32(allocs/op) |
まとめ
今回はencoding/json/v2
のUnmarshalRead
関数とMarshalWrite
関数の簡単な実装例を紹介しました。またベンチマークテストによってv1のUnmarshal
関数と性能を比較し、UnmarshalRead
関数が全ての指標において改善していることを確認しました。
引き続き、encoding/json/v2
のUnmarshalRead
関数とMarshalWrite
関数の理解を深めていきたいと思います。