JSONL en Go : bufio, json.Decoder et concurrence
Un guide complet pour travailler avec les fichiers JSONL (JSON Lines) en Go. Apprenez a lire, ecrire, streamer et traiter simultanement des donnees JSONL en utilisant bufio, encoding/json et les goroutines de la bibliotheque standard.
Derniere mise a jour : fevrier 2026
Pourquoi Go pour le JSONL ?
Go est un excellent choix pour traiter les fichiers JSONL, surtout quand la performance et la concurrence comptent. La bibliotheque standard fournit tout ce dont vous avez besoin : bufio pour des E/S ligne par ligne efficaces, encoding/json pour l'analyse et la serialisation, et les goroutines pour le traitement parallele. Aucune dependance tierce n'est necessaire. La nature compilee de Go signifie que votre pipeline JSONL s'executera beaucoup plus vite que les scripts equivalents en Python ou Node.js, traitant souvent des millions de lignes par seconde sur du materiel modeste.
JSONL (JSON Lines) stocke un objet JSON par ligne, ce qui en fait un format naturel pour le modele d'E/S en streaming de Go. Au lieu de charger un fichier entier en memoire, vous pouvez lire et traiter les enregistrements un par un avec un scanner ou un decodeur. Combine avec les goroutines et les canaux legers de Go, vous pouvez construire des pipelines JSONL concurrents qui utilisent tous les coeurs CPU disponibles. Dans ce guide, vous apprendrez a lire du JSONL avec bufio.Scanner, streamer avec json.Decoder, ecrire du JSONL efficacement, traiter les enregistrements en parallele et gerer les erreurs de maniere robuste.
Lire des fichiers JSONL avec bufio.Scanner
La maniere la plus simple de lire du JSONL en Go est avec bufio.Scanner. Il lit un fichier ligne par ligne, et vous analysez chaque ligne avec json.Unmarshal. Cette approche vous donne un controle total sur la taille des tampons et la gestion des erreurs.
Ouvrez le fichier, creez un scanner et iterez ligne par ligne. Chaque ligne est analysee dans une map ou un struct avec json.Unmarshal. Cela maintient l'utilisation memoire proportionnelle a un seul enregistrement, pas au fichier entier.
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)// Augmenter le tampon pour les lignes de plus de 64 Koscanner.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))}
Pour du code de production, analysez les enregistrements JSONL dans des structs Go types plutot que des maps generiques. Cela fournit une securite de type a la compilation, de meilleures performances et un code plus clair. Definissez votre struct avec des tags json pour controler le mappage des champs.
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 avec json.Decoder
Pour du streaming haute performance, json.Decoder lit directement depuis un io.Reader sans decoupage intermediaire des lignes. Il gere le buffering en interne et constitue l'approche recommandee pour les gros fichiers JSONL ou les flux reseau ou vous souhaitez minimiser les allocations.
json.NewDecoder encapsule n'importe quel io.Reader et decode les valeurs JSON sequentiellement. Chaque appel a Decode lit exactement un objet JSON. Quand le flux se termine, il retourne io.EOF. C'est plus efficace que bufio.Scanner + json.Unmarshal car cela evite de copier chaque ligne dans un tampon separe.
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++// Traiter chaque enregistrement au fur et a mesure du decodageif 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)}}
Ecrire des fichiers JSONL en Go
Ecrire du JSONL en Go est simple : serialisez chaque enregistrement avec json.Marshal, ajoutez un octet de retour a la ligne et ecrivez dans un fichier. Pour de meilleures performances, encapsulez le writer du fichier dans un bufio.Writer pour reduire les appels systeme.
Utilisez json.Marshal pour serialiser chaque enregistrement en octets JSON, puis ecrivez-les suivis d'un retour a la ligne. Cette approche est simple et fonctionne bien pour les fichiers de petite a moyenne taille.
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))}
Pour une ecriture a haut debit, utilisez json.NewEncoder avec un bufio.Writer. L'encodeur ajoute automatiquement un retour a la ligne apres chaque appel a Encode, et le writer bufferise regroupe les petites ecritures en appels systeme plus grands. Appelez toujours Flush avant de fermer le fichier.
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)// Desactiver l'echappement HTML pour une sortie plus propreencoder.SetEscapeHTML(false)for i, event := range events {if err := encoder.Encode(event); err != nil {return fmt.Errorf("encode event %d: %w", i, err)}}// Vider les donnees bufferisees dans le fichierif 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)}}
Traitement JSONL concurrent avec les goroutines
Les goroutines et les canaux de Go facilitent la construction de pipelines JSONL concurrents. Le modele fan-out lit les enregistrements d'un fichier, les distribue a travers plusieurs goroutines de travail et collecte les resultats via un canal. C'est ideal pour les transformations intensives en CPU sur de gros fichiers JSONL.
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 {// Simuler un travail intensif en CPUscore := 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)// Demarrer les workersvar wg sync.WaitGroupfor i := 0; i < numWorkers; i++ {wg.Add(1)go func() {defer wg.Done()for record := range jobs {results <- process(record)}}()}// Fermer le canal des resultats quand tous les workers ont terminego func() {wg.Wait()close(results)}()// Lire le fichier et envoyer les enregistrements aux 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)}()// Collecter et ecrire les resultatsoutFile, 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)}
Ce modele fan-out distribue les enregistrements JSONL a travers des goroutines egales au nombre de coeurs CPU. Les canaux bufferises empechent les goroutines de bloquer sur l'envoi ou la reception. Le sync.WaitGroup garantit que tous les workers ont termine avant la fermeture du canal des resultats. Pour les charges de travail liees aux E/S comme les appels API, vous pouvez augmenter numWorkers au-dela du nombre de CPU. Notez que l'ordre de sortie n'est pas garanti avec le traitement concurrent.
Bonnes pratiques de gestion des erreurs
Un traitement JSONL robuste en Go necessite une gestion attentive des erreurs. Les fichiers JSONL reels peuvent contenir des lignes malformees, des problemes d'encodage ou des enregistrements anormalement grands. Ce modele fournit un suivi des erreurs par ligne, un comportement configurable de saut en cas d'erreur et un rapport de synthese.
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()// Ignorer les lignes videsif 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())}}
Ce struct JSONLReader suit les numeros de ligne, collecte les erreurs d'analyse avec leur contenu brut et produit un rapport de synthese des succes et echecs. Le tampon de 10 Mo du scanner gere les lignes inhabituellement longues sans paniquer. En production, vous pourriez etendre cela avec le support de context.Context pour l'annulation, un seuil d'erreur maximum qui interrompt le traitement, ou un logging structure avec slog.
Packages Go pour le traitement JSONL
La bibliotheque standard de Go fournit tous les elements necessaires pour le traitement JSONL. Voici les trois approches principales et quand utiliser chacune.
bufio.Scanner
FlexibleLe lecteur ligne par ligne standard. S'associe avec json.Unmarshal pour une analyse explicite. Ideal quand vous avez besoin de controler la taille des tampons, de sauter ou inspecter les lignes brutes, ou d'avoir des numeros de ligne pour le rapport d'erreurs. Gere les lignes jusqu'a la taille de tampon configuree.
json.Decoder
RecommandeDecodeur JSON en streaming qui lit directement depuis un io.Reader. Plus efficace que Scanner + Unmarshal car il evite la copie des donnees. Ideal pour les gros fichiers et les flux reseau. Gere automatiquement les valeurs JSON successives sans avoir besoin d'un decoupage explicite des retours a la ligne.
json.Encoder
EcritureEncodeur JSON en streaming qui ecrit dans un io.Writer. Ajoute automatiquement un retour a la ligne apres chaque appel a Encode, ce qui en fait l'outil parfait pour la sortie JSONL. Combinez avec bufio.Writer pour une ecriture a haut debit. Supporte la configuration SetEscapeHTML et SetIndent.
Essayez nos outils JSONL gratuits
Vous ne voulez pas ecrire de code ? Utilisez nos outils en ligne gratuits pour visualiser, valider et convertir des fichiers JSONL directement dans votre navigateur.