JSONL in Go: bufio, json.Decoder e Concorrenza

Una guida completa per lavorare con file JSONL (JSON Lines) in Go. Impara a leggere, scrivere, fare streaming ed elaborare dati JSONL in modo concorrente usando bufio, encoding/json e goroutine della libreria standard.

Ultimo aggiornamento: Febbraio 2026

Perché Go per JSONL?

Go è una scelta eccellente per l'elaborazione di file JSONL, specialmente quando performance e concorrenza sono importanti. La libreria standard fornisce tutto ciò che serve: bufio per I/O efficiente riga per riga, encoding/json per parsing e serializzazione, e goroutine per l'elaborazione parallela. Non sono necessarie dipendenze di terze parti. La natura compilata di Go significa che la tua pipeline JSONL funzionerà significativamente più velocemente rispetto a script equivalenti in Python o Node.js, spesso elaborando milioni di righe al secondo su hardware modesto.

JSONL (JSON Lines) memorizza un oggetto JSON per riga, rendendolo naturalmente adatto al modello di I/O in streaming di Go. Invece di caricare un intero file in memoria, puoi leggere ed elaborare i record uno alla volta usando uno scanner o un decoder. Combinato con le goroutine leggere e i canali di Go, puoi costruire pipeline JSONL concorrenti che saturano tutti i core CPU disponibili. In questa guida imparerai come leggere JSONL con bufio.Scanner, fare streaming con json.Decoder, scrivere JSONL in modo efficiente, elaborare record in modo concorrente e gestire gli errori in modo robusto.

Leggere File JSONL con bufio.Scanner

Il modo più diretto per leggere JSONL in Go è con bufio.Scanner. Legge un file riga per riga e tu analizzi ogni riga con json.Unmarshal. Questo approccio ti dà il pieno controllo sulle dimensioni del buffer e sulla gestione degli errori.

Apri il file, crea uno scanner e itera riga per riga. Ogni riga viene analizzata in una map o struct con json.Unmarshal. Questo mantiene l'uso della memoria proporzionale a un singolo record, non all'intero file.

Lettura Base con 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)
// Aumenta il buffer per righe più lunghe di 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))
}

Per il codice di produzione, analizza i record JSONL in struct Go tipizzate invece di map generiche. Questo fornisce sicurezza dei tipi a tempo di compilazione, migliori prestazioni e codice più chiaro. Definisci la tua struct con tag json per controllare la mappatura dei campi.

Parsing in Struct Tipizzate
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)
}
}

Streaming con json.Decoder

Per lo streaming ad alte prestazioni, json.Decoder legge direttamente da un io.Reader senza separazione intermedia delle righe. Gestisce il buffering internamente ed è l'approccio consigliato per file JSONL di grandi dimensioni o flussi di rete dove si desidera un numero minimo di allocazioni.

json.NewDecoder avvolge qualsiasi io.Reader e decodifica valori JSON in sequenza. Ogni chiamata a Decode legge esattamente un oggetto JSON. Quando il flusso termina, restituisce io.EOF. Questo è più efficiente di bufio.Scanner + json.Unmarshal perché evita di copiare ogni riga in un buffer separato.

Streaming con 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++
// Elabora ogni record mentre viene decodificato
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)
}
}

Scrivere File JSONL in Go

Scrivere JSONL in Go è semplice: serializza ogni record con json.Marshal, aggiungi un byte di nuova riga e scrivi su un file. Per le migliori prestazioni, avvolgi il writer del file in un bufio.Writer per ridurre le chiamate di sistema.

Usa json.Marshal per serializzare ogni record in byte JSON, poi scrivili seguiti da una nuova riga. Questo approccio è semplice e funziona bene per file di piccole e medie dimensioni.

Scrittura Base con 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))
}

Per la scrittura ad alto throughput, usa json.NewEncoder con un bufio.Writer. L'encoder aggiunge automaticamente una nuova riga dopo ogni chiamata Encode, e il writer bufferizzato raggruppa le piccole scritture in chiamate di sistema più grandi. Chiama sempre Flush prima di chiudere il file.

Scrittura Bufferizzata con 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)
// Disabilita l'escaping HTML per un output più pulito
encoder.SetEscapeHTML(false)
for i, event := range events {
if err := encoder.Encode(event); err != nil {
return fmt.Errorf("encode event %d: %w", i, err)
}
}
// Scrivi i dati bufferizzati nel 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)
}
}

Elaborazione JSONL Concorrente con Goroutine

Le goroutine e i canali di Go rendono facile costruire pipeline JSONL concorrenti. Il pattern fan-out legge i record da un file, li distribuisce tra più goroutine worker e raccoglie i risultati attraverso un canale. Questo è ideale per trasformazioni CPU-bound su file JSONL di grandi dimensioni.

Elaborazione JSONL Concorrente con Goroutine
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 {
// Simula lavoro CPU-bound
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)
// Avvia i worker
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)
}
}()
}
// Chiudi il canale dei risultati quando tutti i worker terminano
go func() {
wg.Wait()
close(results)
}()
// Leggi il file e invia i record ai worker
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)
}()
// Raccogli e scrivi i risultati
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)
}

Questo pattern fan-out distribuisce i record JSONL tra goroutine pari al numero di core CPU. I canali bufferizzati impediscono alle goroutine di bloccarsi in invio o ricezione. Il sync.WaitGroup assicura che tutti i worker terminino prima che il canale dei risultati venga chiuso. Per carichi di lavoro I/O-bound come chiamate API, puoi aumentare numWorkers oltre il conteggio CPU. Nota che l'ordine dell'output non è garantito con l'elaborazione concorrente.

Best Practice per la Gestione degli Errori

Un'elaborazione JSONL robusta in Go richiede una gestione attenta degli errori. I file JSONL del mondo reale possono contenere righe malformate, problemi di codifica o record inaspettatamente grandi. Questo pattern fornisce tracciamento degli errori a livello di riga, comportamento configurabile di skip-on-error e report di riepilogo.

Best Practice per la Gestione degli Errori
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()
// Salta le righe vuote
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())
}
}

Questa struct JSONLReader traccia i numeri di riga, raccoglie gli errori di parsing con il loro contenuto grezzo e produce un riepilogo di successi e fallimenti. Il buffer dello scanner da 10MB gestisce righe insolitamente grandi senza andare in panic. In produzione, potresti estendere questo con supporto context.Context per la cancellazione, una soglia massima di errori che interrompe l'elaborazione, o logging strutturato con slog.

Pacchetti Go per l'Elaborazione JSONL

La libreria standard di Go fornisce tutti i componenti necessari per l'elaborazione JSONL. Ecco i tre approcci principali e quando usare ciascuno.

bufio.Scanner

Flessibile

Il lettore standard riga per riga. Si abbina con json.Unmarshal per il parsing esplicito. Ideale quando hai bisogno di controllare le dimensioni del buffer, vuoi saltare o ispezionare righe grezze, o hai bisogno dei numeri di riga per la segnalazione degli errori. Gestisce righe fino alla dimensione del buffer configurata.

json.Decoder

Consigliato

Decoder JSON in streaming che legge direttamente da un io.Reader. Più efficiente di Scanner + Unmarshal perché evita di copiare i dati. Ideale per file grandi e flussi di rete. Gestisce automaticamente valori JSON consecutivi senza necessità di separazione esplicita delle nuove righe.

json.Encoder

Scrittura

Encoder JSON in streaming che scrive su un io.Writer. Aggiunge automaticamente una nuova riga dopo ogni chiamata Encode, rendendolo perfetto per l'output JSONL. Combinalo con bufio.Writer per la scrittura ad alto throughput. Supporta la configurazione SetEscapeHTML e SetIndent.

Prova i Nostri Strumenti JSONL Gratuiti

Non vuoi scrivere codice? Usa i nostri strumenti online gratuiti per visualizzare, validare e convertire file JSONL direttamente nel tuo browser.

Lavora con File JSONL Online

Visualizza, valida e converti file JSONL fino a 1GB direttamente nel tuo browser. Nessun upload necessario, 100% privato.

Domande Frequenti

JSONL in Go — bufio.Scanner, json.Decoder e Concorrenza |...