Go で JSON ストリームを処理する:Encoder と Decoder の使い方
大量の JSON データを扱うとき、json.Marshal / json.Unmarshal ではメモリ上にすべてのデータを展開する必要があります。json.Encoder と json.Decoder を使えば、ストリーム形式で少しずつ読み書きでき、メモリ消費を抑えられます。
Encoder と Decoder の基本
json.NewEncoder は io.Writer に JSON を書き出し、json.NewDecoder は io.Reader から JSON を読み取ります。HTTP ハンドラでの利用がもっとも一般的です。
func createUser(w http.ResponseWriter, r *http.Request) {
// リクエストボディから JSON を読み取る
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "JSON が不正です", http.StatusBadRequest)
return
}
user := User{ID: 1, Name: req.Name, Email: req.Email}
// レスポンスに JSON を書き出す
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}http.Request.Body は io.Reader を、http.ResponseWriter は io.Writer を満たしているため、直接渡せます。Encode は構造体を JSON に変換して書き込み、末尾に改行を付けます。Decode は Reader から JSON を一つ読み取って構造体にパースします。
Marshal / Unmarshal との違い
json.Marshal はデータ全体を []byte に変換してから返します。一方 json.Encoder は io.Writer に直接書き出すため、中間バッファが不要です。
データ全体をメモリ上の []byte に変換する。小〜中規模の JSON に適している。変換結果を変数として保持できる。
io.Writer / io.Reader に対してストリーム処理する。大規模データやネットワーク I/O に適している。中間バッファが不要でメモリ効率が高い。
たとえば 100MB の JSON ファイルを json.Unmarshal で処理すると、ファイル内容の []byte とパース結果の構造体で合計 200MB 以上のメモリを消費します。Decoder ならファイルから直接パースするため、[]byte の分を節約できます。
複数の JSON を連続して読む
json.Decoder の真価は、一つの Reader から複数の JSON 値を順番に読み取れることです。NDJSON(Newline Delimited JSON)やログファイルの処理に向いています。
input := `{"name":"Alice","email":"alice@example.com"}
{"name":"Bob","email":"bob@example.com"}
{"name":"Charlie","email":"charlie@example.com"}`
decoder := json.NewDecoder(strings.NewReader(input))
for decoder.More() {
var user User
if err := decoder.Decode(&user); err != nil {
break
}
fmt.Printf("%s (%s)\n", user.Name, user.Email)
}decoder.More() は次に読み取れる値があるかを返します。EOF まで繰り返し Decode を呼ぶことで、ストリーム内のすべてのデータを 1 件ずつ処理できます。100 万行の NDJSON ファイルでも、メモリは 1 件分で済むわけです。
Reader から JSON を 1 件読み取り
Decode で構造体にパース
decoder.More() で次があるか確認
データがなくなるまで繰り返し
Token で巨大配列を分解する
通常の Decode だと、JSON 配列は一括でスライスにパースされます。巨大な配列の要素を一つずつ処理したい場合は Token() を併用します。
func processLargeArray(r io.Reader) error {
decoder := json.NewDecoder(r)
// '[' を読む
if _, err := decoder.Token(); err != nil {
return err
}
for decoder.More() {
var user User
if err := decoder.Decode(&user); err != nil {
return err
}
fmt.Println(user.Name)
}
// ']' を読む
decoder.Token()
return nil
}Token() は JSON の構造トークン([、]、{、} や文字列キーなど)を一つ読み取ります。配列の開始 [ を消費した後、More() と Decode で要素を一つずつ取り出し、最後に終了 ] を消費する流れです。
配列全体をスライスにパースしないため、メモリ使用量が要素数に依存しません。数百万件のデータエクスポートでもメモリが安定します。
Decoder の便利メソッド
Decoder にはパース挙動を変更するメソッドがいくつか用意されています。
decoder := json.NewDecoder(r.Body)
// 構造体にないフィールドがあればエラー
decoder.DisallowUnknownFields()
// 数値を float64 でなく json.Number として扱う
decoder.UseNumber()DisallowUnknownFields は API のバリデーションに役立ちます。クライアントがタイポしたフィールドを送ってきたときにエラーを返せるので、問題を早期に検出できます。
UseNumber は大きな整数を扱う場面で重要です。デフォルトでは JSON の数値は float64 に変換されますが、53 ビットを超える整数値では精度が失われます。json.Number を使えば文字列として保持し、Int64() や Float64() で安全に変換できます。
decoder := json.NewDecoder(strings.NewReader(`{"id":9007199254740993}`))
decoder.UseNumber()
var result map[string]interface{}
decoder.Decode(&result)
num := result["id"].(json.Number)
id, _ := num.Int64()
fmt.Println(id) // 9007199254740993(精度が保たれる)Encoder のオプション
encoder := json.NewEncoder(os.Stdout)
// 整形出力
encoder.SetIndent("", " ")
// HTML エスケープを無効化
encoder.SetEscapeHTML(false)SetIndent はデバッグ用の出力で便利ですが、本番の API レスポンスではインデントなしが一般的です。空白の分だけレスポンスサイズが増えるため、帯域の無駄になります。
SetEscapeHTML(false) は <、>、& の Unicode エスケープを無効にします。デフォルトでは XSS 対策としてこれらがエスケープされますが、純粋な API レスポンスではエスケープが不要な場合もあるでしょう。
| SetIndent | 整形出力(デバッグ・設定ファイル向き) |
| SetEscapeHTML(false) | HTML エスケープ無効化(API 向き) |
ファイル I/O との組み合わせ
os.File は io.Reader と io.Writer の両方を満たしているため、ファイルの読み書きにもそのまま使えます。
// 書き込み
func saveUsers(users []User, path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return json.NewEncoder(file).Encode(users)
}
// 読み取り
func loadUsers(path string) ([]User, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var users []User
err = json.NewDecoder(file).Decode(&users)
return users, err
}ioutil.ReadAll でファイル全体を []byte に読み込んでから Unmarshal する方法と比べて、コードがシンプルになりメモリ効率も向上します。日常的な JSON の読み書きでは、最初から Encoder / Decoder を使う癖をつけておくとよいでしょう。











