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.

Lectura basica 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)
// Increase buffer for lines longer than 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))
}

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.

Analisis en structs tipados
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

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.

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++
// Process each record as it's decoded
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)
}
}

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.

Escritura basica 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))
}

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.

Escritura con buffer usando 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)
// Disable HTML escaping for cleaner output
encoder.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 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)
}
}

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.

Procesamiento concurrente de JSONL con 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 {
// Simulate CPU-bound work
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)
}
}()
}
// Close results channel when all workers finish
go func() {
wg.Wait()
close(results)
}()
// Read file and send records to workers
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)
}()
// Collect and write results
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)
}

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.

Mejores practicas de manejo de errores
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()
// Skip empty lines
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())
}
}

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

Flexible

El 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

Recomendado

Decodificador 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

Escritura

Codificador 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.

Trabaja con archivos JSONL online

Visualiza, valida y convierte archivos JSONL de hasta 1GB directamente en tu navegador. Sin subidas necesarias, 100% privado.

Preguntas frecuentes

JSONL en Go — bufio.Scanner, json.Decoder y concurrencia ...