JSONL in Go: bufio, json.Decoder & Nebenläufigkeit
Eine vollständige Anleitung zur Arbeit mit JSONL (JSON Lines)-Dateien in Go. Lernen Sie das Lesen, Schreiben, Streamen und nebenläufige Verarbeiten von JSONL-Daten mit den Standardbibliotheken bufio, encoding/json und Goroutines.
Letzte Aktualisierung: Februar 2026
Warum Go für JSONL?
Go ist eine ausgezeichnete Wahl für die Verarbeitung von JSONL-Dateien, insbesondere wenn Leistung und Nebenläufigkeit wichtig sind. Die Standardbibliothek bietet alles, was Sie brauchen: bufio für effiziente zeilenweise I/O, encoding/json für Parsing und Serialisierung sowie Goroutines für parallele Verarbeitung. Es sind keine Drittanbieter-Abhängigkeiten erforderlich. Die kompilierte Natur von Go bedeutet, dass Ihre JSONL-Pipeline deutlich schneller läuft als vergleichbare Python- oder Node.js-Skripte und oft Millionen von Zeilen pro Sekunde auf bescheidener Hardware verarbeitet.
JSONL (JSON Lines) speichert ein JSON-Objekt pro Zeile und ist damit eine natürliche Ergänzung zu Go's Streaming-I/O-Modell. Anstatt eine gesamte Datei in den Speicher zu laden, können Sie Datensätze einzeln mit einem Scanner oder Decoder lesen und verarbeiten. In Kombination mit Go's leichtgewichtigen Goroutines und Channels können Sie nebenläufige JSONL-Pipelines erstellen, die alle verfügbaren CPU-Kerne auslasten. In dieser Anleitung lernen Sie, wie Sie JSONL mit bufio.Scanner lesen, mit json.Decoder streamen, JSONL effizient schreiben, Datensätze nebenläufig verarbeiten und Fehler robust behandeln.
JSONL-Dateien mit bufio.Scanner lesen
Der einfachste Weg, JSONL in Go zu lesen, ist mit bufio.Scanner. Er liest eine Datei Zeile für Zeile, und Sie parsen jede Zeile mit json.Unmarshal. Dieser Ansatz gibt Ihnen volle Kontrolle über Puffergrößen und Fehlerbehandlung.
Öffnen Sie die Datei, erstellen Sie einen Scanner und iterieren Sie Zeile für Zeile. Jede Zeile wird mit json.Unmarshal in eine Map oder ein Struct geparst. Dies hält den Speicherverbrauch proportional zu einem einzelnen Datensatz, nicht zur gesamten Datei.
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)// Puffer für Zeilen länger als 64KB vergrößernscanner.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))}
Für Produktionscode sollten Sie JSONL-Datensätze in typisierte Go-Structs parsen, anstatt generische Maps zu verwenden. Dies bietet Typsicherheit zur Kompilierzeit, bessere Leistung und klareren Code. Definieren Sie Ihr Struct mit json-Tags, um das Feld-Mapping zu steuern.
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)}}
Streaming mit json.Decoder
Für leistungsstarkes Streaming liest json.Decoder direkt von einem io.Reader ohne zwischengeschaltete Zeilentrennung. Er übernimmt das Buffering intern und ist der empfohlene Ansatz für große JSONL-Dateien oder Netzwerk-Streams, bei denen Sie minimale Allokationen wünschen.
json.NewDecoder umhüllt jeden io.Reader und dekodiert JSON-Werte sequenziell. Jeder Aufruf von Decode liest genau ein JSON-Objekt. Wenn der Stream endet, gibt er io.EOF zurück. Dies ist effizienter als bufio.Scanner + json.Unmarshal, da das Kopieren jeder Zeile in einen separaten Puffer vermieden wird.
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++// Jeden Datensatz beim Dekodieren verarbeitenif 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)}}
JSONL-Dateien in Go schreiben
Das Schreiben von JSONL in Go ist unkompliziert: Serialisieren Sie jeden Datensatz mit json.Marshal, hängen Sie ein Newline-Byte an und schreiben Sie in eine Datei. Für beste Leistung umhüllen Sie den File-Writer mit einem bufio.Writer, um Systemaufrufe zu reduzieren.
Verwenden Sie json.Marshal, um jeden Datensatz in JSON-Bytes zu serialisieren, und schreiben Sie diese gefolgt von einem Newline. Dieser Ansatz ist einfach und funktioniert gut für kleine bis mittlere Dateien.
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))}
Für das Schreiben mit hohem Durchsatz verwenden Sie json.NewEncoder mit einem bufio.Writer. Der Encoder hängt nach jedem Encode-Aufruf automatisch ein Newline an, und der gepufferte Writer fasst kleine Schreibvorgänge zu größeren Systemaufrufen zusammen. Rufen Sie immer Flush auf, bevor Sie die Datei schließen.
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)// HTML-Escaping für sauberere Ausgabe deaktivierenencoder.SetEscapeHTML(false)for i, event := range events {if err := encoder.Encode(event); err != nil {return fmt.Errorf("encode event %d: %w", i, err)}}// Gepufferte Daten in die Datei schreibenif 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)}}
Nebenläufige JSONL-Verarbeitung mit Goroutines
Go's Goroutines und Channels machen es einfach, nebenläufige JSONL-Pipelines zu erstellen. Das Fan-Out-Muster liest Datensätze aus einer Datei, verteilt sie auf mehrere Worker-Goroutines und sammelt Ergebnisse über einen Channel. Dies ist ideal für CPU-intensive Transformationen großer JSONL-Dateien.
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 {// CPU-intensive Arbeit simulierenscore := 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)// Worker startenvar wg sync.WaitGroupfor i := 0; i < numWorkers; i++ {wg.Add(1)go func() {defer wg.Done()for record := range jobs {results <- process(record)}}()}// Results-Channel schließen wenn alle Worker fertig sindgo func() {wg.Wait()close(results)}()// Datei lesen und Datensätze an Worker sendengo 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)}()// Ergebnisse sammeln und schreibenoutFile, 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)}
Dieses Fan-Out-Muster verteilt JSONL-Datensätze auf Goroutines, deren Anzahl der CPU-Kerne entspricht. Die gepufferten Channels verhindern, dass Goroutines beim Senden oder Empfangen blockieren. Die sync.WaitGroup stellt sicher, dass alle Worker fertig sind, bevor der Results-Channel geschlossen wird. Für I/O-intensive Workloads wie API-Aufrufe können Sie numWorkers über die CPU-Anzahl hinaus erhöhen. Beachten Sie, dass die Ausgabereihenfolge bei nebenläufiger Verarbeitung nicht garantiert ist.
Best Practices für die Fehlerbehandlung
Robuste JSONL-Verarbeitung in Go erfordert sorgfältige Fehlerbehandlung. JSONL-Dateien in der Praxis können fehlerhafte Zeilen, Kodierungsprobleme oder unerwartet große Datensätze enthalten. Dieses Muster bietet Fehlerverfolgung auf Zeilenebene, konfigurierbares Überspringen bei Fehlern und zusammenfassende Berichterstattung.
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()// Leere Zeilen überspringenif 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())}}
Dieses JSONLReader-Struct verfolgt Zeilennummern, sammelt Parse-Fehler mit ihrem Rohinhalt und gibt eine Zusammenfassung von Erfolgen und Fehlern aus. Der 10-MB-Scanner-Puffer verarbeitet ungewöhnlich große Zeilen, ohne abzustürzen. In der Produktion könnten Sie dies mit context.Context-Unterstützung für Abbruch, einer maximalen Fehlerschwelle zum Abbrechen der Verarbeitung oder strukturiertem Logging mit slog erweitern.
Go-Pakete für die JSONL-Verarbeitung
Go's Standardbibliothek bietet alle Bausteine, die Sie für die JSONL-Verarbeitung benötigen. Hier sind die drei Hauptansätze und wann Sie welchen verwenden sollten.
bufio.Scanner
FlexibelDer standardmäßige zeilenweise Reader. Wird mit json.Unmarshal für explizites Parsen kombiniert. Am besten geeignet, wenn Sie Kontrolle über Puffergrößen benötigen, Rohzeilen überspringen oder inspizieren möchten oder Zeilennummern für die Fehlerberichterstattung brauchen. Verarbeitet Zeilen bis zur konfigurierten Puffergröße.
json.Decoder
EmpfohlenStreaming-JSON-Decoder, der direkt von einem io.Reader liest. Effizienter als Scanner + Unmarshal, da das Kopieren von Daten vermieden wird. Ideal für große Dateien und Netzwerk-Streams. Verarbeitet aufeinanderfolgende JSON-Werte automatisch ohne explizite Newline-Trennung.
json.Encoder
SchreibenStreaming-JSON-Encoder, der in einen io.Writer schreibt. Hängt nach jedem Encode-Aufruf automatisch ein Newline an, was ihn perfekt für JSONL-Ausgabe macht. Kombinieren Sie ihn mit bufio.Writer für Schreiben mit hohem Durchsatz. Unterstützt SetEscapeHTML- und SetIndent-Konfiguration.
Testen Sie unsere kostenlosen JSONL-Tools
Möchten Sie keinen Code schreiben? Verwenden Sie unsere kostenlosen Online-Tools, um JSONL-Dateien direkt in Ihrem Browser anzuzeigen, zu validieren und zu konvertieren.