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 にパースされます。これにより、メモリ使用量はファイル全体ではなく、単一レコードに比例します。

bufio.Scanner による基本的な読み取り
package main
import (
"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]any
scanner := bufio.NewScanner(file)
// Increase buffer for lines longer than 64KB
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
for scanner.Scan() {
var record map[string]any
if 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 main
import (
"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 []User
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
var u User
if 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 よりも効率的です。

json.Decoder によるストリーミング
package main
import (
"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 int
for {
var entry LogEntry
err := 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 decoded
if 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 バイトにシリアライズし、改行に続けて書き込みます。このアプローチはシンプルで、小〜中規模のファイルに適しています。

json.Marshal による基本的な書き込み
package main
import (
"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 を呼び出してください。

json.Encoder によるバッファード書き込み
package main
import (
"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 output
encoder.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 file
if 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 バウンドな変換に最適です。

goroutine による並行 JSONL 処理
package main
import (
"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 work
score := len(r.Data) * r.ID
return 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 workers
var wg sync.WaitGroup
for 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 finish
go func() {
wg.Wait()
close(results)
}()
// Read file and send records to workers
go func() {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
var r Record
if err := json.Unmarshal(scanner.Bytes(), &r); err != nil {
log.Printf("skip invalid line: %v", err)
continue
}
jobs <- r
}
close(jobs)
}()
// Collect and write results
outFile, err := os.Create("results.jsonl")
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
writer := bufio.NewWriter(outFile)
encoder := json.NewEncoder(writer)
var count int
for 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 main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type ParseError struct {
Line int
Err error
Raw string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("line %d: %v", e.Line, e.Err)
}
type JSONLReader struct {
scanner *bufio.Scanner
lineNum int
Errors []ParseError
Skipped int
Success 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 lines
if len(line) == 0 {
continue
}
var record map[string]any
if 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]any
if 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 ファイルの表示、検証、変換ができます。

オンラインで JSONL ファイルを操作

ブラウザで最大 1GB の JSONL ファイルを表示、検証、変換。アップロード不要、100% プライベート。

よくある質問

Go で JSONL — bufio.Scanner、json.Decoder&並行処理 | jsonl.co