🍣

Go 1.25 の JSONv2 と HTTP Client

に公開

本記事は、Go 1.25で試験導入されている JSONv2 の検証記事です。あと、おまけでHTTP Clientについて語ります。

https://go.dev/doc/go1.25#json_v2

新規のインターフェースが追加されたこともありますが、中でも既存のendoding/jsonの実装が改良され、特にJSONデコードのパフォーマンスが向上したようです。

公式のパフォーマンス・テスト結果はこちらにあります。

インストール方法

2025年8月時点ではまだ rcリリースになります。

私は goenv 経由でインストールしました。以下で、go env GOPATHは go のパスになります。

go install golang.org/dl/go1.25rc2@latest
ls $(go env GOPATH)/bin

この段階ではまだダウンロードされてません。downloadを実行します。

$(go env GOPATH)/bin/go1.25rc2 download
$(go env GOPATH)/bin/go1.25rc2 version

動作確認のため、ChatGPTに main.go を適当に作成してもらい、実行してみましょう。

$(go env GOPATH)/bin/go1.25rc2 run main.go

実装

Unmarshal

GoではJSONデータとGoのstruct型の変換をマーシャリングと呼んでいます。一方、RustやC#ではシリアライズと称しています。

ではさっそく、json.Unmarshalのベンチマーク・プログラムを書いてパフォーマンスを検証してみましょう。

unmarshal
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// deserializeJSON is a function that takes a JSON byte slice and unmarshals it into a Person struct.
func main() {
	// JSON data (as a string for this example)
	jsonStr := `{"name":"Alice","age":25}`
	jsonBytes := []byte(jsonStr)

	// Create a Person struct to hold the deserialized data
	var tPerson Person

	// Deserialize JSON to struct with v2 semantics
	err := json.Unmarshal(jsonBytes, &tPerson)
	if err != nil {
		log.Fatal("Error deserializing JSON:", err)
	}

	// Print the deserialized struct
	fmt.Printf("Deserialized: %+v\n", tPerson)
}

encoding/jsonはドロップイン・リプレースメントで、ソースの修正なくそのまま動きます。
環境変数GOEXPERIMENT=jsonv2を設定すれば有効になります。

go bench
GOEXPERIMENT=jsonv2 $(go env GOPATH)/bin/go1.25rc2 test -bench=.

まとめ職人の ChatGPT 様に以下のようにまとめていただきました。ご覧のように 67% の速度向上が観測できました。いきな絵文字までつけて、芸が細いです。

多言語との比較

参考のため、PythonとNode.jsでも試してみました。

Python

デフォのjsonライブラリと、Rust製のorjsonで試してみました。
やはり、jsonの場合は劇おそですが、orjsonGo V1より速くなりました。さすがRustです。

ご承知のように、Python(Cpython)のintは型ヒントであり、CやRustのようなプリミティブなintではなく、PyObjectでありメモリ・レイアウトは複雑になります。numpyやPolarsのように、CやRustのプリミティブ型で処理することで高速化することができます。

unmarshal
import json
from dataclasses import dataclass
import timeit

import orjson


@dataclass
class Person:
    name: str
    age: int


json_str = '{"name":"Alice","age":25}'
json_bytes = json_str.encode('utf-8')  # UTF-8 is the most common and default


def deserialize():
    data = json.loads(json_str)
    person = Person(**data)


def deserialize2():
    data = orjson.loads(json_bytes)
    person = Person(**data)


if __name__ == "__main__":
    duration = timeit.timeit(deserialize, number=1_000_000)
    print(f"Total time: {duration:.4f}s")
    print(f"Average per call: {duration / 1_000_000 * 1e6:.2f} µs")

    duration = timeit.timeit(deserialize2, number=1_000_000)
    print(f"Total time: {duration:.4f}s")
    print(f"Average per call: {duration / 1_000_000 * 1e6:.2f} µs")

実行結果です。上がjson、下がorjsonです。

Node.js

Node.jsはイベント・ループを使用したシングル・スレッドのランタイムです。したがって、CPUヘビィな処理はイベント・ループをチョークさせてしまうため御法度です。

外部APIやデータベースからのペイロードのパースは、Node.jsの全体のスループットを悪化させる原因になります。ペイロードのサイズが大きい場合は、JSONStreamなどでストリーミング処理することでメモリの消費を抑え高速化することができます。

unmarshal
const jsonStr = '{"name":"Alice","age":25}';

class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

function deserialize(): void {
    const data: { name: string; age: number } = JSON.parse(jsonStr);
    const person = new Person(data.name, data.age);
}

// Benchmark like Python's timeit
const iterations = 1_000_000;
const start = performance.now();

for (let i = 0; i < iterations; i++) {
    deserialize();
}

const end = performance.now();
const durationSeconds = (end - start) / 1000;

console.log(`Total time: ${durationSeconds.toFixed(4)}s`);
console.log(`Average per call: ${(durationSeconds / iterations * 1e6).toFixed(2)} µs`);

実行結果です。Node.js v22を使用しています。Rust製のorjsonと同じ速さになりました。

結果
Total time: 0.3518s
Average per call: 0.35 µs

以上の結果から、Go JSONv2は他の言語と比較しても突出して速くなっているのが分かりました。

おまけ: HTTP Client

Goの標準ライブラリは非常に充実していますが、リトライ処理サーキットブレーカーなどの機能は標準では提供されていません。ここでは、ライブラリreqを使用した例を紹介します。

リトライ

データセンターをまたぐHTTP呼び出しでは、途中に多数のネットワーク機器(ルーターやロードバランサーなど)を経由するため、ごく稀に 502や504といったエラーが発生する可能性 があります。
そのため、こうした一時的な失敗を見越してリトライ処理を実装しておくことで、システム全体の可用性を高めることができます。

リトライの間隔は、指数関数的に徐々に長く、かつ揺らぎを与えることで、タイミングをずらすことで、リクエストが集中しないようにします。そうでないと、リトライ処理自体がシステム障害を引き起こす可能性があるからです。

SetRetryCountでリトライ回数を指定します。SetRetryBackoffIntervalでバックオフの間隔を指定します。

reqでのリトライ
package main

import (
	"fmt"
	"github.com/imroc/req/v3"
	"time"
)

type Post struct {
	ID     int    `json:"id"`
	UserID int    `json:"userId"`
	Title  string `json:"title"`
	Body   string `json:"body"`
}
type ErrorMessage struct {
	// Message is the error message returned by the API
	// NB. jsonplaceholder.typicode.com does not return error messages
	Message string `json:"message"`
}

func main() {

	client := req.NewClient()
	var posts []Post
	var errMsg ErrorMessage

	start := time.Now()
	resp, err := client.
		SetTimeout(10*time.Second).
		R().
		SetRetryCount(2).
		// Set the retry sleep interval with a commonly used algorithm: capped exponential backoff with jitter (https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/).
		SetRetryBackoffInterval(1*time.Second, 5*time.Second).
		EnableDumpWithoutHeader().
		SetSuccessResult(&posts).
		SetErrorResult(&errMsg).
		Get("https://jsonplaceholder.typicode.com/posts")

	duration := time.Since(start)
	fmt.Printf("Fetch duration: %v\n", duration)

	if err != nil {
		fmt.Println("Error sending request:", err)
		return
	}

	if resp.IsErrorState() {
		fmt.Println("Request failed with status:", resp.Status)
		fmt.Println(errMsg.Message) // Record error message returned.
		return
	}

	fmt.Printf("Total posts: %d\n", len(posts))
	for i := 0; i < 5; i++ { // show first 5
		fmt.Printf("Post %d: %+v\n", i+1, posts[i])
	}
}

サーキット・ブレーカー

GCPやAWSのクラウドはしばしばネットワーク障害を起こします。皆さんが思うほど、インターネットやクラウドのネットワークは盤石、安定ではなく、突発的な遮断はしばしば発生します。

一定のエラーが発生した場合に、早期にリトライを中断し、ほとぼりが冷めるまで一時的にリクエストを停止することで、サーバーへの余計なトラフィックを遮断します。
火に油を注ぐようなものですので、中途半端なリトライ設計はリスクがあります。

Sonyのsony/gobreakerを使用した例です。
公式のサンプルのコードがこちらにあります。

https://github.com/sony/gobreaker/blob/master/v2/example/http_breaker.go

高階関数になっており、ターゲットの関数をcb.Execute(req func() (T, error))に渡せば良いです。公式サンプルでは、単純にhttp.GET()を呼び出す関数を渡しています。
インターセプターの作りになっており、前処理と後処理でサンドイッチする実装になっています。

gobreaker.CircuitBreakerは状態を保持する必要があるため、ステート・マシーンになっており、グローバル変数として定義します。そのため、スレッドセーフな作りになっており、内部で排他制御しています。

Discussion