Go で JSONL を扱う:bufio、json.Decoder と並行処理
Go で JSONL(JSON Lines)ファイルを扱うための完全ガイド。標準ライブラリの bufio、encoding/json、goroutine を使用して、JSONL データの読み取り、書き込み、ストリーミング、並行処理を学びます。
最終更新:2026年2月
なぜ Go で JSONL を処理するのか?
Go はパフォーマンスと並行性が重要な JSONL ファイル処理に優れた選択肢です。標準ライブラリには必要なものがすべて揃っています:効率的な行単位 I/O のための bufio、パースとシリアライズのための encoding/json、並列処理のための goroutine。サードパーティの依存関係は不要です。Go はコンパイル言語であるため、JSONL パイプラインは同等の Python や Node.js スクリプトよりも大幅に高速に実行され、控えめなハードウェアでも毎秒数百万行を処理できることがよくあります。
JSONL(JSON Lines)は1行に1つの JSON オブジェクトを格納するため、Go のストリーミング I/O モデルに自然にフィットします。ファイル全体をメモリにロードする代わりに、スキャナーまたはデコーダーを使用してレコードを1つずつ読み取り、処理できます。Go の軽量 goroutine とチャネルを組み合わせることで、利用可能なすべての CPU コアを活用する並行 JSONL パイプラインを構築できます。このガイドでは、bufio.Scanner で JSONL を読み取る方法、json.Decoder でストリーミングする方法、効率的に JSONL を書き込む方法、レコードを並行処理する方法、堅牢なエラーハンドリングについて学びます。
bufio.Scanner で JSONL ファイルを読み取る
Go で JSONL を読み取る最も簡単な方法は bufio.Scanner です。ファイルを1行ずつ読み取り、各行を json.Unmarshal でパースします。このアプローチにより、バッファサイズとエラーハンドリングを完全に制御できます。
ファイルを開き、スキャナーを作成して、1行ずつイテレートします。各行は json.Unmarshal で map または struct にパースされます。これにより、メモリ使用量はファイル全体ではなく、単一レコードに比例します。
package mainimport ("bufio""encoding/json""fmt""log""os")func main() {file, err := os.Open("data.jsonl")if err != nil {log.Fatal(err)}defer file.Close()var records []map[string]anyscanner := bufio.NewScanner(file)// Increase buffer for lines longer than 64KBscanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)for scanner.Scan() {var record map[string]anyif err := json.Unmarshal(scanner.Bytes(), &record); err != nil {log.Printf("skipping invalid JSON: %v", err)continue}records = append(records, record)}if err := scanner.Err(); err != nil {log.Fatal(err)}fmt.Printf("Loaded %d records\n", len(records))}
本番コードでは、汎用的な map ではなく、型付き Go 構造体に JSONL レコードをパースします。これにより、コンパイル時の型安全性、より良いパフォーマンス、そしてより明確なコードが得られます。json タグ付きの構造体を定義してフィールドマッピングを制御します。
package mainimport ("bufio""encoding/json""fmt""log""os")type User struct {ID int `json:"id"`Name string `json:"name"`Email string `json:"email"`Age int `json:"age,omitempty"`}func readUsers(path string) ([]User, error) {file, err := os.Open(path)if err != nil {return nil, fmt.Errorf("open file: %w", err)}defer file.Close()var users []Userscanner := bufio.NewScanner(file)lineNum := 0for scanner.Scan() {lineNum++var u Userif err := json.Unmarshal(scanner.Bytes(), &u); err != nil {return nil, fmt.Errorf("line %d: %w", lineNum, err)}users = append(users, u)}if err := scanner.Err(); err != nil {return nil, fmt.Errorf("scanner error: %w", err)}return users, nil}func main() {users, err := readUsers("users.jsonl")if err != nil {log.Fatal(err)}for _, u := range users {fmt.Printf("%s (%s)\n", u.Name, u.Email)}}
json.Decoder によるストリーミング
高パフォーマンスなストリーミングのために、json.Decoder は中間的な行分割なしで io.Reader から直接読み取ります。内部でバッファリングを処理するため、大きな JSONL ファイルやネットワークストリームでアロケーションを最小限に抑えたい場合に推奨されるアプローチです。
json.NewDecoder は任意の io.Reader をラップし、JSON 値を順次デコードします。Decode を呼び出すたびに、正確に1つの JSON オブジェクトが読み取られます。ストリームが終了すると io.EOF を返します。これは各行を別のバッファにコピーする必要がないため、bufio.Scanner + json.Unmarshal よりも効率的です。
package mainimport ("encoding/json""errors""fmt""io""log""os")type LogEntry struct {Timestamp string `json:"timestamp"`Level string `json:"level"`Message string `json:"message"`Service string `json:"service"`}func processJSONLStream(path string) error {file, err := os.Open(path)if err != nil {return fmt.Errorf("open: %w", err)}defer file.Close()decoder := json.NewDecoder(file)var count intfor {var entry LogEntryerr := decoder.Decode(&entry)if errors.Is(err, io.EOF) {break}if err != nil {return fmt.Errorf("decode record %d: %w", count+1, err)}count++// Process each record as it's decodedif entry.Level == "ERROR" {fmt.Printf("[%s] %s: %s\n",entry.Timestamp, entry.Service, entry.Message)}}fmt.Printf("Processed %d log entries\n", count)return nil}func main() {if err := processJSONLStream("logs.jsonl"); err != nil {log.Fatal(err)}}
Go で JSONL ファイルを書き込む
Go で JSONL を書き込むのは簡単です:各レコードを json.Marshal でシリアライズし、改行バイトを追加してファイルに書き込みます。最高のパフォーマンスを得るために、ファイルライターを bufio.Writer でラップしてシステムコールを削減します。
json.Marshal を使用して各レコードを JSON バイトにシリアライズし、改行に続けて書き込みます。このアプローチはシンプルで、小〜中規模のファイルに適しています。
package mainimport ("encoding/json""fmt""log""os")type Product struct {ID int `json:"id"`Name string `json:"name"`Price float64 `json:"price"`}func main() {products := []Product{{ID: 1, Name: "Laptop", Price: 999.99},{ID: 2, Name: "Mouse", Price: 29.99},{ID: 3, Name: "Keyboard", Price: 79.99},}file, err := os.Create("products.jsonl")if err != nil {log.Fatal(err)}defer file.Close()for _, p := range products {data, err := json.Marshal(p)if err != nil {log.Printf("skip marshal error: %v", err)continue}data = append(data, '\n')if _, err := file.Write(data); err != nil {log.Fatal(err)}}fmt.Printf("Wrote %d records to products.jsonl\n", len(products))}
高スループットの書き込みには、json.NewEncoder と bufio.Writer を使用します。エンコーダーは各 Encode 呼び出しの後に自動的に改行を追加し、バッファードライターは小さな書き込みをまとめて大きなシステムコールにバッチ処理します。ファイルを閉じる前に必ず Flush を呼び出してください。
package mainimport ("bufio""encoding/json""fmt""log""os")type Event struct {Type string `json:"type"`Timestamp int64 `json:"timestamp"`Payload map[string]any `json:"payload"`}func writeEvents(path string, events []Event) error {file, err := os.Create(path)if err != nil {return fmt.Errorf("create file: %w", err)}defer file.Close()writer := bufio.NewWriter(file)encoder := json.NewEncoder(writer)// Disable HTML escaping for cleaner outputencoder.SetEscapeHTML(false)for i, event := range events {if err := encoder.Encode(event); err != nil {return fmt.Errorf("encode event %d: %w", i, err)}}// Flush buffered data to fileif err := writer.Flush(); err != nil {return fmt.Errorf("flush: %w", err)}fmt.Printf("Wrote %d events to %s\n", len(events), path)return nil}func main() {events := []Event{{Type: "click", Timestamp: 1700000001, Payload: map[string]any{"x": 120, "y": 450}},{Type: "view", Timestamp: 1700000002, Payload: map[string]any{"page": "/home"}},}if err := writeEvents("events.jsonl", events); err != nil {log.Fatal(err)}}
goroutine による並行 JSONL 処理
Go の goroutine とチャネルにより、並行 JSONL パイプラインを簡単に構築できます。ファンアウトパターンはファイルからレコードを読み取り、複数のワーカー goroutine に分配し、チャネルを通じて結果を収集します。これは大きな JSONL ファイルの CPU バウンドな変換に最適です。
package mainimport ("bufio""encoding/json""fmt""log""os""runtime""sync")type Record struct {ID int `json:"id"`Data map[string]any `json:"data"`}type Result struct {ID int `json:"id"`Processed bool `json:"processed"`Score int `json:"score"`}func process(r Record) Result {// Simulate CPU-bound workscore := len(r.Data) * r.IDreturn Result{ID: r.ID, Processed: true, Score: score}}func main() {file, err := os.Open("records.jsonl")if err != nil {log.Fatal(err)}defer file.Close()numWorkers := runtime.NumCPU()jobs := make(chan Record, numWorkers*2)results := make(chan Result, numWorkers*2)// Start workersvar wg sync.WaitGroupfor i := 0; i < numWorkers; i++ {wg.Add(1)go func() {defer wg.Done()for record := range jobs {results <- process(record)}}()}// Close results channel when all workers finishgo func() {wg.Wait()close(results)}()// Read file and send records to workersgo func() {scanner := bufio.NewScanner(file)for scanner.Scan() {var r Recordif err := json.Unmarshal(scanner.Bytes(), &r); err != nil {log.Printf("skip invalid line: %v", err)continue}jobs <- r}close(jobs)}()// Collect and write resultsoutFile, err := os.Create("results.jsonl")if err != nil {log.Fatal(err)}defer outFile.Close()writer := bufio.NewWriter(outFile)encoder := json.NewEncoder(writer)var count intfor result := range results {if err := encoder.Encode(result); err != nil {log.Printf("encode error: %v", err)}count++}writer.Flush()fmt.Printf("Processed %d records with %d workers\n", count, numWorkers)}
このファンアウトパターンは、CPU コア数に等しい goroutine に JSONL レコードを分配します。バッファード チャネルにより、goroutine が送受信でブロックされるのを防ぎます。sync.WaitGroup はすべてのワーカーが完了してから results チャネルが閉じられることを保証します。API 呼び出しなどの I/O バウンドなワークロードでは、numWorkers を CPU 数以上に増やすことができます。並行処理では出力順序が保証されないことに注意してください。
エラーハンドリングのベストプラクティス
Go で堅牢な JSONL 処理を行うには、慎重なエラーハンドリングが必要です。実世界の JSONL ファイルには、不正な行、エンコーディングの問題、予想外に大きなレコードが含まれる可能性があります。このパターンは、行レベルのエラー追跡、設定可能なスキップ動作、サマリーレポートを提供します。
package mainimport ("bufio""encoding/json""fmt""log""os")type ParseError struct {Line intErr errorRaw string}func (e *ParseError) Error() string {return fmt.Sprintf("line %d: %v", e.Line, e.Err)}type JSONLReader struct {scanner *bufio.ScannerlineNum intErrors []ParseErrorSkipped intSuccess int}func NewJSONLReader(file *os.File) *JSONLReader {scanner := bufio.NewScanner(file)scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)return &JSONLReader{scanner: scanner}}func (r *JSONLReader) ReadAll(target any) error {slice, ok := target.(*[]map[string]any)if !ok {return fmt.Errorf("target must be *[]map[string]any")}for r.scanner.Scan() {r.lineNum++line := r.scanner.Bytes()// Skip empty linesif len(line) == 0 {continue}var record map[string]anyif err := json.Unmarshal(line, &record); err != nil {r.Errors = append(r.Errors, ParseError{Line: r.lineNum,Err: err,Raw: string(line),})r.Skipped++continue}*slice = append(*slice, record)r.Success++}return r.scanner.Err()}func (r *JSONLReader) Summary() string {return fmt.Sprintf("Lines: %d | Success: %d | Skipped: %d | Errors: %d",r.lineNum, r.Success, r.Skipped, len(r.Errors),)}func main() {file, err := os.Open("data.jsonl")if err != nil {log.Fatal(err)}defer file.Close()reader := NewJSONLReader(file)var records []map[string]anyif err := reader.ReadAll(&records); err != nil {log.Fatal(err)}fmt.Println(reader.Summary())for _, e := range reader.Errors {log.Printf("Parse error at %s", e.Error())}}
この JSONLReader 構造体は行番号を追跡し、生データ付きのパースエラーを収集し、成功と失敗のサマリーをレポートします。10MB のスキャナーバッファにより、異常に大きな行もパニックなく処理できます。本番環境では、キャンセル用の context.Context サポート、処理を中断する最大エラー閾値、または slog による構造化ログなどで拡張することを検討してください。
JSONL 処理用の Go パッケージ
Go の標準ライブラリには JSONL 処理に必要なすべてのビルディングブロックが揃っています。以下に3つのコアアプローチとそれぞれの使い所を紹介します。
bufio.Scanner
柔軟標準的な行単位リーダー。明示的なパースのために json.Unmarshal と組み合わせます。バッファサイズの制御、生の行のスキップや検査、エラーレポート用の行番号が必要な場合に最適です。設定したバッファサイズまでの行を処理できます。
json.Decoder
推奨io.Reader から直接読み取るストリーミング JSON デコーダー。データのコピーを避けるため、Scanner + Unmarshal よりも効率的です。大きなファイルやネットワークストリームに最適。明示的な改行分割なしで連続した JSON 値を自動的に処理します。
json.Encoder
書き込みio.Writer に書き込むストリーミング JSON エンコーダー。各 Encode 呼び出しの後に自動的に改行を追加するため、JSONL 出力に最適です。高スループットの書き込みには bufio.Writer と組み合わせます。SetEscapeHTML と SetIndent の設定をサポートしています。
無料 JSONL ツールを試す
コードを書きたくない場合は、無料のオンラインツールでブラウザから直接 JSONL ファイルの表示、検証、変換ができます。