JSONL in Go: bufio, json.Decoder & Concurrency

Een complete handleiding voor het werken met JSONL (JSON Lines) bestanden in Go. Leer hoe je JSONL-data leest, schrijft, streamt en gelijktijdig verwerkt met de standaardbibliotheek's bufio, encoding/json en goroutines.

Laatst bijgewerkt: februari 2026

Waarom Go voor JSONL?

Go is een uitstekende keuze voor het verwerken van JSONL-bestanden, vooral wanneer prestaties en concurrency belangrijk zijn. De standaardbibliotheek biedt alles wat je nodig hebt: bufio voor efficiënte regel-voor-regel I/O, encoding/json voor parsing en serialisatie, en goroutines voor parallelle verwerking. Er zijn geen externe afhankelijkheden nodig. Omdat Go gecompileerd is, draait je JSONL-pipeline aanzienlijk sneller dan vergelijkbare Python- of Node.js-scripts, en verwerkt vaak miljoenen regels per seconde op bescheiden hardware.

JSONL (JSON Lines) slaat één JSON-object per regel op, wat een natuurlijke match is met Go's streaming I/O-model. In plaats van een heel bestand in het geheugen te laden, kun je records één voor één lezen en verwerken met een scanner of decoder. Gecombineerd met Go's lichtgewicht goroutines en channels kun je gelijktijdige JSONL-pipelines bouwen die alle beschikbare CPU-cores benutten. In deze handleiding leer je hoe je JSONL leest met bufio.Scanner, streamt met json.Decoder, JSONL efficiënt schrijft, records gelijktijdig verwerkt en fouten robuust afhandelt.

JSONL-bestanden lezen met bufio.Scanner

De eenvoudigste manier om JSONL in Go te lezen is met bufio.Scanner. Het leest een bestand regel voor regel, en je parst elke regel met json.Unmarshal. Deze aanpak geeft je volledige controle over buffergroottes en foutafhandeling.

Open het bestand, maak een scanner aan en itereer regel voor regel. Elke regel wordt geparseerd naar een map of struct met json.Unmarshal. Dit houdt het geheugengebruik evenredig aan één enkel record, niet het hele bestand.

Basis lezen met 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)
// Vergroot de buffer voor regels langer dan 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("ongeldige JSON overgeslagen: %v", err)
continue
}
records = append(records, record)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("%d records geladen\n", len(records))
}

Voor productiecode, parseer JSONL-records naar getypeerde Go-structs in plaats van generieke maps. Dit biedt compile-time typeveiligheid, betere prestaties en duidelijkere code. Definieer je struct met json-tags om de veldmapping te bepalen.

Parseren naar getypeerde structs
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("bestand openen: %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("regel %d: %w", lineNum, err)
}
users = append(users, u)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scannerfout: %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 met json.Decoder

Voor hoogwaardige streaming leest json.Decoder rechtstreeks van een io.Reader zonder tussentijdse regelsplitsing. Het handelt buffering intern af en is de aanbevolen aanpak voor grote JSONL-bestanden of netwerkstreams waarbij je minimale allocaties wilt.

json.NewDecoder wikkelt elke io.Reader en decodeert JSON-waarden sequentieel. Elke aanroep van Decode leest precies één JSON-object. Wanneer de stream eindigt, retourneert het io.EOF. Dit is efficiënter dan bufio.Scanner + json.Unmarshal omdat het het kopiëren van elke regel naar een aparte buffer vermijdt.

Streaming met 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("openen: %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("decoderen record %d: %w", count+1, err)
}
count++
// Verwerk elk record terwijl het gedecodeerd wordt
if entry.Level == "ERROR" {
fmt.Printf("[%s] %s: %s\n",
entry.Timestamp, entry.Service, entry.Message)
}
}
fmt.Printf("%d logvermeldingen verwerkt\n", count)
return nil
}
func main() {
if err := processJSONLStream("logs.jsonl"); err != nil {
log.Fatal(err)
}
}

JSONL-bestanden schrijven in Go

JSONL schrijven in Go is eenvoudig: serialiseer elk record met json.Marshal, voeg een newline-byte toe en schrijf naar een bestand. Voor de beste prestaties wikkel je de bestandswriter in een bufio.Writer om het aantal systeemaanroepen te verminderen.

Gebruik json.Marshal om elk record naar JSON-bytes te serialiseren, schrijf ze vervolgens gevolgd door een newline. Deze aanpak is eenvoudig en werkt goed voor kleine tot middelgrote bestanden.

Basis schrijven met 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("marshal-fout overgeslagen: %v", err)
continue
}
data = append(data, '\n')
if _, err := file.Write(data); err != nil {
log.Fatal(err)
}
}
fmt.Printf("%d records geschreven naar products.jsonl\n", len(products))
}

Voor hoge doorvoer bij het schrijven, gebruik json.NewEncoder met een bufio.Writer. De encoder voegt automatisch een newline toe na elke Encode-aanroep, en de gebufferde writer groepeert kleine schrijfacties in grotere systeemaanroepen. Roep altijd Flush aan voordat je het bestand sluit.

Gebufferd schrijven met 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("bestand aanmaken: %w", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
encoder := json.NewEncoder(writer)
// Schakel HTML-escaping uit voor schonere output
encoder.SetEscapeHTML(false)
for i, event := range events {
if err := encoder.Encode(event); err != nil {
return fmt.Errorf("event %d encoderen: %w", i, err)
}
}
// Flush gebufferde data naar bestand
if err := writer.Flush(); err != nil {
return fmt.Errorf("flush: %w", err)
}
fmt.Printf("%d events geschreven naar %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)
}
}

Gelijktijdige JSONL-verwerking met Goroutines

Go's goroutines en channels maken het eenvoudig om gelijktijdige JSONL-pipelines te bouwen. Het fan-out-patroon leest records uit een bestand, verdeelt ze over meerdere worker-goroutines en verzamelt resultaten via een channel. Dit is ideaal voor CPU-intensieve transformaties op grote JSONL-bestanden.

Gelijktijdige JSONL-verwerking met Goroutines
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 {
// Simuleer CPU-intensief werk
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)
}
}()
}
// Sluit results-channel wanneer alle workers klaar zijn
go func() {
wg.Wait()
close(results)
}()
// Lees bestand en stuur records naar workers
go func() {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
var r Record
if err := json.Unmarshal(scanner.Bytes(), &r); err != nil {
log.Printf("ongeldige regel overgeslagen: %v", err)
continue
}
jobs <- r
}
close(jobs)
}()
// Verzamel en schrijf resultaten
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("coderingsfout: %v", err)
}
count++
}
writer.Flush()
fmt.Printf("%d records verwerkt met %d workers\n", count, numWorkers)
}

Dit fan-out-patroon verdeelt JSONL-records over goroutines gelijk aan het aantal CPU-cores. De gebufferde channels voorkomen dat goroutines blokkeren bij verzenden of ontvangen. De sync.WaitGroup zorgt ervoor dat alle workers klaar zijn voordat het results-channel gesloten wordt. Voor I/O-intensieve workloads zoals API-aanroepen kun je numWorkers verhogen boven het CPU-aantal. Let op dat de uitvoervolgorde niet gegarandeerd is bij gelijktijdige verwerking.

Best practices voor foutafhandeling

Robuuste JSONL-verwerking in Go vereist zorgvuldige foutafhandeling. JSONL-bestanden in de praktijk kunnen misvormde regels, coderingsproblemen of onverwacht grote records bevatten. Dit patroon biedt foutopsporing op regelniveau, configureerbaar overslaan bij fouten en samenvattingsrapportage.

Best practices voor foutafhandeling
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("regel %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 moet *[]map[string]any zijn")
}
for r.scanner.Scan() {
r.lineNum++
line := r.scanner.Bytes()
// Sla lege regels over
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(
"Regels: %d | Geslaagd: %d | Overgeslagen: %d | Fouten: %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("Parseerfout op %s", e.Error())
}
}

Deze JSONLReader-struct houdt regelnummers bij, verzamelt parseerfouten met hun ruwe inhoud en rapporteert een samenvatting van successen en mislukkingen. De 10MB scannerbuffer verwerkt ongewoon grote regels zonder te crashen. In productie kun je dit uitbreiden met context.Context-ondersteuning voor annulering, een maximale foutdrempel die de verwerking afbreekt, of gestructureerde logging met slog.

Go-pakketten voor JSONL-verwerking

Go's standaardbibliotheek biedt alle bouwstenen die je nodig hebt voor JSONL-verwerking. Hier zijn de drie kernbenaderingen en wanneer je ze moet gebruiken.

bufio.Scanner

Flexibel

De standaard regel-voor-regel reader. Gecombineerd met json.Unmarshal voor expliciete parsing. Het beste wanneer je controle nodig hebt over buffergroottes, ruwe regels wilt overslaan of inspecteren, of regelnummers nodig hebt voor foutrapportage. Verwerkt regels tot je geconfigureerde buffergrootte.

json.Decoder

Aanbevolen

Streaming JSON-decoder die rechtstreeks van een io.Reader leest. Efficiënter dan Scanner + Unmarshal omdat het het kopiëren van data vermijdt. Ideaal voor grote bestanden en netwerkstreams. Verwerkt opeenvolgende JSON-waarden automatisch zonder expliciete newline-splitsing.

json.Encoder

Schrijven

Streaming JSON-encoder die naar een io.Writer schrijft. Voegt automatisch een newline toe na elke Encode-aanroep, wat het perfect maakt voor JSONL-output. Combineer met bufio.Writer voor hoge doorvoer bij schrijven. Ondersteunt SetEscapeHTML en SetIndent configuratie.

Probeer onze gratis JSONL-tools

Wil je geen code schrijven? Gebruik onze gratis online tools om JSONL-bestanden te bekijken, valideren en converteren direct in je browser.

Werk online met JSONL-bestanden

Bekijk, valideer en converteer JSONL-bestanden tot 1GB direct in je browser. Geen uploads nodig, 100% privé.

Veelgestelde vragen

JSONL in Go — bufio.Scanner, json.Decoder & concurrency |...