JSONL en Go: bufio, json.Decoder y Concurrencia
Una guia completa para trabajar con archivos JSONL (JSON Lines) en Go. Aprende a leer, escribir, transmitir y procesar concurrentemente datos JSONL usando bufio, encoding/json y goroutines de la biblioteca estandar.
Ultima actualizacion: febrero 2026
¿Por que Go para JSONL?
Go es una excelente opcion para procesar archivos JSONL, especialmente cuando el rendimiento y la concurrencia importan. La biblioteca estandar proporciona todo lo necesario: bufio para E/S eficiente linea por linea, encoding/json para analisis y serializacion, y goroutines para procesamiento paralelo. No se requieren dependencias de terceros. La naturaleza compilada de Go significa que tu pipeline JSONL se ejecutara significativamente mas rapido que scripts equivalentes en Python o Node.js, procesando a menudo millones de lineas por segundo en hardware modesto.
JSONL (JSON Lines) almacena un objeto JSON por linea, lo que lo convierte en un ajuste natural para el modelo de E/S en streaming de Go. En lugar de cargar un archivo completo en memoria, puedes leer y procesar registros uno a la vez usando un scanner o decoder. Combinado con las goroutines livianas y los canales de Go, puedes construir pipelines JSONL concurrentes que saturan todos los nucleos de CPU disponibles. En esta guia, aprenderas como leer JSONL con bufio.Scanner, transmitir con json.Decoder, escribir JSONL eficientemente, procesar registros concurrentemente y manejar errores de forma robusta.
Lectura de archivos JSONL con bufio.Scanner
La forma mas directa de leer JSONL en Go es con bufio.Scanner. Lee un archivo linea por linea y analizas cada linea con json.Unmarshal. Este enfoque te da control total sobre los tamanos de buffer y el manejo de errores.
Abre el archivo, crea un scanner e itera linea por linea. Cada linea se analiza en un map o struct con json.Unmarshal. Esto mantiene el uso de memoria proporcional a un solo registro, no al archivo completo.
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)// Increase buffer for lines longer than 64KBscanner.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))}
Para codigo de produccion, analiza los registros JSONL en structs tipados de Go en lugar de maps genericos. Esto proporciona seguridad de tipos en tiempo de compilacion, mejor rendimiento y codigo mas claro. Define tu struct con etiquetas json para controlar el mapeo de campos.
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 con json.Decoder
Para streaming de alto rendimiento, json.Decoder lee directamente desde un io.Reader sin division intermedia de lineas. Maneja el buffering internamente y es el enfoque recomendado para archivos JSONL grandes o flujos de red donde deseas minimizar las asignaciones de memoria.
json.NewDecoder envuelve cualquier io.Reader y decodifica valores JSON secuencialmente. Cada llamada a Decode lee exactamente un objeto JSON. Cuando el flujo termina, devuelve io.EOF. Esto es mas eficiente que bufio.Scanner + json.Unmarshal porque evita copiar cada linea en un buffer separado.
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++// Process each record as it's decodedif 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)}}
Escritura de archivos JSONL en Go
Escribir JSONL en Go es sencillo: serializa cada registro con json.Marshal, agrega un byte de nueva linea y escribe en un archivo. Para el mejor rendimiento, envuelve el escritor de archivos en un bufio.Writer para reducir las llamadas al sistema.
Usa json.Marshal para serializar cada registro a bytes JSON, luego escribelos seguidos de una nueva linea. Este enfoque es simple y funciona bien para archivos pequenos a medianos.
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))}
Para escritura de alto rendimiento, usa json.NewEncoder con un bufio.Writer. El encoder agrega una nueva linea despues de cada llamada a Encode automaticamente, y el escritor con buffer agrupa escrituras pequenas en llamadas al sistema mas grandes. Siempre llama a Flush antes de cerrar el archivo.
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)// Disable HTML escaping for cleaner outputencoder.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 fileif 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)}}
Procesamiento concurrente de JSONL con Goroutines
Las goroutines y los canales de Go facilitan la construccion de pipelines JSONL concurrentes. El patron fan-out lee registros de un archivo, los distribuye entre multiples goroutines de trabajo y recopila resultados a traves de un canal. Esto es ideal para transformaciones intensivas en CPU sobre archivos JSONL grandes.
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 {// Simulate CPU-bound workscore := 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)// Start workersvar wg sync.WaitGroupfor 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 finishgo func() {wg.Wait()close(results)}()// Read file and send records to 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)}()// Collect and write resultsoutFile, 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)}
Este patron fan-out distribuye registros JSONL entre goroutines iguales al numero de nucleos de CPU. Los canales con buffer evitan que las goroutines se bloqueen en envio o recepcion. El sync.WaitGroup asegura que todos los workers terminen antes de cerrar el canal de resultados. Para cargas de trabajo con E/S intensiva como llamadas a APIs, puedes aumentar numWorkers mas alla del conteo de CPU. Ten en cuenta que el orden de salida no esta garantizado con procesamiento concurrente.
Mejores practicas de manejo de errores
El procesamiento robusto de JSONL en Go requiere un manejo cuidadoso de errores. Los archivos JSONL del mundo real pueden contener lineas malformadas, problemas de codificacion o registros inesperadamente grandes. Este patron proporciona seguimiento de errores a nivel de linea, comportamiento configurable de omision en error e informes resumidos.
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()// Skip empty linesif 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())}}
Este struct JSONLReader rastrea numeros de linea, recopila errores de analisis con su contenido sin procesar e informa un resumen de exitos y fallos. El buffer de scanner de 10MB maneja lineas inusualmente grandes sin entrar en panico. En produccion, podrias extender esto con soporte de context.Context para cancelacion, un umbral maximo de errores que aborta el procesamiento, o registro estructurado con slog.
Paquetes de Go para procesamiento JSONL
La biblioteca estandar de Go proporciona todos los bloques de construccion que necesitas para el procesamiento JSONL. Aqui estan los tres enfoques principales y cuando usar cada uno.
bufio.Scanner
FlexibleEl lector estandar linea por linea. Se combina con json.Unmarshal para analisis explicito. Ideal cuando necesitas control sobre tamanos de buffer, quieres omitir o inspeccionar lineas sin procesar, o necesitas numeros de linea para informes de errores. Maneja lineas hasta el tamano de buffer configurado.
json.Decoder
RecomendadoDecodificador JSON en streaming que lee directamente desde un io.Reader. Mas eficiente que Scanner + Unmarshal porque evita copiar datos. Ideal para archivos grandes y flujos de red. Maneja valores JSON consecutivos automaticamente sin necesidad de division explicita por nueva linea.
json.Encoder
EscrituraCodificador JSON en streaming que escribe a un io.Writer. Agrega automaticamente una nueva linea despues de cada llamada a Encode, lo que lo hace perfecto para la salida JSONL. Combinalo con bufio.Writer para escritura de alto rendimiento. Soporta configuracion SetEscapeHTML y SetIndent.
Prueba nuestras herramientas JSONL gratuitas
¿No quieres escribir codigo? Usa nuestras herramientas online gratuitas para ver, validar y convertir archivos JSONL directamente en tu navegador.