Go 1.25 の JSONv2 と HTTP Client
本記事は、Go 1.25で試験導入されている JSONv2 の検証記事です。あと、おまけでHTTP Clientについて語ります。
新規のインターフェースが追加されたこともありますが、中でも既存の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
のベンチマーク・プログラムを書いてパフォーマンスを検証してみましょう。
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
を設定すれば有効になります。
GOEXPERIMENT=jsonv2 $(go env GOPATH)/bin/go1.25rc2 test -bench=.
まとめ職人の ChatGPT 様に以下のようにまとめていただきました。ご覧のように 67% の速度向上が観測できました。いきな絵文字までつけて、芸が細いです。
多言語との比較
参考のため、PythonとNode.jsでも試してみました。
Python
デフォのjson
ライブラリと、Rust製のorjson
で試してみました。
やはり、json
の場合は劇おそですが、orjson
はGo V1より速くなりました。さすがRustです。
ご承知のように、Python(Cpython)のintは型ヒントであり、CやRustのようなプリミティブなintではなく、PyObjectでありメモリ・レイアウトは複雑になります。numpyやPolarsのように、CやRustのプリミティブ型で処理することで高速化することができます。
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
などでストリーミング処理することでメモリの消費を抑え高速化することができます。
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
でバックオフの間隔を指定します。
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を使用した例です。
公式のサンプルのコードがこちらにあります。
高階関数になっており、ターゲットの関数をcb.Execute(req func() (T, error))
に渡せば良いです。公式サンプルでは、単純にhttp.GET()
を呼び出す関数を渡しています。
インターセプターの作りになっており、前処理と後処理でサンドイッチする実装になっています。
gobreaker.CircuitBreaker
は状態を保持する必要があるため、ステート・マシーンになっており、グローバル変数として定義します。そのため、スレッドセーフな作りになっており、内部で排他制御しています。
Discussion