英語614435 views
高校日本史190647 views
雑学1473736 views
ヒストリア291495 views
りんご212052 views
数学講師2891552 views
LaTeX962830 views
中学英語812106 views
高校倫理1441073 views
中学理科1631340 views
Help
Tools

English

Go で JSON ストリームを処理する:Encoder と Decoder の使い方

大量の JSON データを扱うとき、json.Marshal / json.Unmarshal ではメモリ上にすべてのデータを展開する必要があります。json.Encoderjson.Decoder を使えば、ストリーム形式で少しずつ読み書きでき、メモリ消費を抑えられます。

Encoder と Decoder の基本

json.NewEncoderio.Writer に JSON を書き出し、json.NewDecoderio.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.Bodyio.Reader を、http.ResponseWriterio.Writer を満たしているため、直接渡せます。Encode は構造体を JSON に変換して書き込み、末尾に改行を付けます。Decode は Reader から JSON を一つ読み取って構造体にパースします。

Marshal / Unmarshal との違い

json.Marshal はデータ全体を []byte に変換してから返します。一方 json.Encoderio.Writer に直接書き出すため、中間バッファが不要です。

Marshal / Unmarshal

データ全体をメモリ上の []byte に変換する。小〜中規模の JSON に適している。変換結果を変数として保持できる。

Encoder / Decoder

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.Fileio.Readerio.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 を使う癖をつけておくとよいでしょう。