1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Golang]encoding/json/v2のUnmarshalRead関数/MarshalWrite関数がいい感じ

Last updated at Posted at 2025-09-01

本記事でやること

今回はGo1.25から実験的に導入された(今後標準パッケージとして採用される可能性がある)encoding/json/v2UnmarshalRead関数MarshalWrite関数を使った簡単な実装例をencoding/json (v1)と比較しながら紹介します。また、ベンチマーク機能によってUnmarshalRead関数とUnmarshal関数(v1)の性能比較をします。

今回実装したコードは以下のレポジトリで公開しています。

対象読者

  • encoding/json/v2UnmarshalRead関数/MarshalWrite関数が気になっている方

使用言語

  • Go 1.25.0

UnmarshalRead/MarshalWrite関数の定義

encoding/json/v2UnmarshalRead関数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関数を用いた場合と比較し、中間バッファを必要としないのでメモリ使用量の削減に繋がりパフォーマンスの向上が期待できます。

v2
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関数を用いた処理を以下に記述します。

v1
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構造体をそのまま受け取り直接ファイルへの書き込みが可能です。

v2
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関数を用いた処理を以下に記述します。

v1
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をそのまま受け取ることができます。

v2
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関数へ入力する必要があります。

v1
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関数は受け取ることができます。

v2
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
    }
})
v1
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/v2UnmarshalRead関数とencoding/jsonUnmarshal関数の性能をベンチーマーク機能を使って比較します。

Go1.25でGOEXPERIMENT=jsonv2を有効にするとencoding/jsonのUnmarshal関数は内部的にv2のUnmarshal関数を呼ぶことになるので、ベンチマークのファイルを2つに分け、v1のUnmarshal関数を実行する際はGOEXPERIMENT=jsonv2を有効にせず実行します。

benchmark_v2_test.go
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
benchmark_v1_test.go
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/v2UnmarshalRead関数とMarshalWrite関数の簡単な実装例を紹介しました。またベンチマークテストによってv1のUnmarshal関数と性能を比較し、UnmarshalRead関数が全ての指標において改善していることを確認しました。

引き続き、encoding/json/v2UnmarshalRead関数とMarshalWrite関数の理解を深めていきたいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?