JSONL em Go: bufio, json.Decoder e Concorrência

Um guia completo para trabalhar com arquivos JSONL (JSON Lines) em Go. Aprenda a ler, escrever, transmitir e processar dados JSONL de forma concorrente usando bufio, encoding/json e goroutines da biblioteca padrão.

Última atualização: fevereiro de 2026

Por que Go para JSONL?

Go é uma excelente escolha para processar arquivos JSONL, especialmente quando desempenho e concorrência são importantes. A biblioteca padrão fornece tudo que você precisa: bufio para I/O eficiente linha por linha, encoding/json para parsing e serialização, e goroutines para processamento paralelo. Não são necessárias dependências de terceiros. A natureza compilada do Go significa que seu pipeline JSONL será significativamente mais rápido que scripts equivalentes em Python ou Node.js, frequentemente processando milhões de linhas por segundo em hardware modesto.

JSONL (JSON Lines) armazena um objeto JSON por linha, tornando-o uma combinação natural com o modelo de I/O streaming do Go. Em vez de carregar um arquivo inteiro na memória, você pode ler e processar registros um por vez usando um scanner ou decoder. Combinado com as goroutines leves e channels do Go, você pode construir pipelines JSONL concorrentes que saturam todos os núcleos de CPU disponíveis. Neste guia, você aprenderá como ler JSONL com bufio.Scanner, transmitir com json.Decoder, escrever JSONL de forma eficiente, processar registros de forma concorrente e tratar erros de maneira robusta.

Lendo Arquivos JSONL com bufio.Scanner

A maneira mais direta de ler JSONL em Go é com bufio.Scanner. Ele lê um arquivo linha por linha, e você faz o parsing de cada linha com json.Unmarshal. Essa abordagem oferece controle total sobre tamanhos de buffer e tratamento de erros.

Abra o arquivo, crie um scanner e itere linha por linha. Cada linha é parseada em um map ou struct com json.Unmarshal. Isso mantém o uso de memória proporcional a um único registro, não ao arquivo inteiro.

Leitura Básica com 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 o buffer para linhas maiores que 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("pulando JSON inválido: %v", err)
continue
}
records = append(records, record)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("Carregados %d registros\n", len(records))
}

Para código de produção, faça o parsing de registros JSONL em structs Go tipadas em vez de maps genéricos. Isso fornece segurança de tipos em tempo de compilação, melhor desempenho e código mais claro. Defina sua struct com tags json para controlar o mapeamento de campos.

Parsing em Structs Tipadas
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("abrir arquivo: %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("linha %d: %w", lineNum, err)
}
users = append(users, u)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("erro do scanner: %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 com json.Decoder

Para streaming de alto desempenho, json.Decoder lê diretamente de um io.Reader sem divisão intermediária de linhas. Ele gerencia o buffering internamente e é a abordagem recomendada para arquivos JSONL grandes ou streams de rede onde você deseja alocações mínimas.

json.NewDecoder encapsula qualquer io.Reader e decodifica valores JSON sequencialmente. Cada chamada a Decode lê exatamente um objeto JSON. Quando o stream termina, retorna io.EOF. Isso é mais eficiente que bufio.Scanner + json.Unmarshal porque evita copiar cada linha em um buffer separado.

Streaming com 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("abrir: %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("decodificar registro %d: %w", count+1, err)
}
count++
// Processa cada registro conforme é decodificado
if entry.Level == "ERROR" {
fmt.Printf("[%s] %s: %s\n",
entry.Timestamp, entry.Service, entry.Message)
}
}
fmt.Printf("Processadas %d entradas de log\n", count)
return nil
}
func main() {
if err := processJSONLStream("logs.jsonl"); err != nil {
log.Fatal(err)
}
}

Escrevendo Arquivos JSONL em Go

Escrever JSONL em Go é simples: serialize cada registro com json.Marshal, adicione um byte de nova linha e escreva no arquivo. Para melhor desempenho, envolva o writer do arquivo em um bufio.Writer para reduzir chamadas de sistema.

Use json.Marshal para serializar cada registro em bytes JSON, depois escreva-os seguidos de uma nova linha. Essa abordagem é simples e funciona bem para arquivos pequenos a médios.

Escrita Básica com 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("pular erro de marshal: %v", err)
continue
}
data = append(data, '\n')
if _, err := file.Write(data); err != nil {
log.Fatal(err)
}
}
fmt.Printf("Escritos %d registros em products.jsonl\n", len(products))
}

Para escrita de alto desempenho, use json.NewEncoder com um bufio.Writer. O encoder adiciona uma nova linha após cada chamada Encode automaticamente, e o writer com buffer agrupa escritas pequenas em chamadas de sistema maiores. Sempre chame Flush antes de fechar o arquivo.

Escrita com 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("criar arquivo: %w", err)
}
defer file.Close()
writer := bufio.NewWriter(file)
encoder := json.NewEncoder(writer)
// Desabilita escape de HTML para saída mais limpa
encoder.SetEscapeHTML(false)
for i, event := range events {
if err := encoder.Encode(event); err != nil {
return fmt.Errorf("codificar evento %d: %w", i, err)
}
}
// Flush dos dados no buffer para o arquivo
if err := writer.Flush(); err != nil {
return fmt.Errorf("flush: %w", err)
}
fmt.Printf("Escritos %d eventos em %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)
}
}

Processamento Concorrente de JSONL com Goroutines

As goroutines e channels do Go facilitam a construção de pipelines JSONL concorrentes. O padrão fan-out lê registros de um arquivo, distribui-os entre múltiplas goroutines worker e coleta resultados através de um channel. Isso é ideal para transformações que exigem CPU em arquivos JSONL grandes.

Processamento Concorrente de JSONL com 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 {
// Simula trabalho intensivo de CPU
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)
// Inicia os 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)
}
}()
}
// Fecha o channel de resultados quando todos os workers terminarem
go func() {
wg.Wait()
close(results)
}()
// Lê o arquivo e envia registros para os workers
go func() {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
var r Record
if err := json.Unmarshal(scanner.Bytes(), &r); err != nil {
log.Printf("pulando linha inválida: %v", err)
continue
}
jobs <- r
}
close(jobs)
}()
// Coleta e escreve os resultados
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("erro ao codificar: %v", err)
}
count++
}
writer.Flush()
fmt.Printf("Processados %d registros com %d workers\n", count, numWorkers)
}

Este padrão fan-out distribui registros JSONL entre goroutines iguais ao número de núcleos de CPU. Os channels com buffer evitam que goroutines fiquem bloqueadas ao enviar ou receber. O sync.WaitGroup garante que todos os workers terminem antes que o channel de resultados seja fechado. Para cargas de trabalho ligadas a I/O, como chamadas de API, você pode aumentar numWorkers além da contagem de CPUs. Note que a ordem de saída não é garantida com processamento concorrente.

Boas Práticas de Tratamento de Erros

O processamento robusto de JSONL em Go requer tratamento cuidadoso de erros. Arquivos JSONL do mundo real podem conter linhas malformadas, problemas de codificação ou registros inesperadamente grandes. Este padrão fornece rastreamento de erros por linha, comportamento configurável de pular-em-erro e relatórios resumidos.

Boas Práticas de Tratamento de Erros
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("linha %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 deve ser *[]map[string]any")
}
for r.scanner.Scan() {
r.lineNum++
line := r.scanner.Bytes()
// Pula linhas vazias
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(
"Linhas: %d | Sucesso: %d | Puladas: %d | Erros: %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("Erro de parse em %s", e.Error())
}
}

Esta struct JSONLReader rastreia números de linha, coleta erros de parse com seu conteúdo bruto e reporta um resumo de sucessos e falhas. O buffer de 10MB do scanner lida com linhas incomumente grandes sem causar panic. Em produção, você pode estender isso com suporte a context.Context para cancelamento, um limite máximo de erros que aborta o processamento ou logging estruturado com slog.

Pacotes Go para Processamento JSONL

A biblioteca padrão do Go fornece todos os blocos de construção necessários para processamento JSONL. Aqui estão as três abordagens principais e quando usar cada uma.

bufio.Scanner

Flexível

O leitor linha por linha padrão. Combina com json.Unmarshal para parsing explícito. Melhor quando você precisa de controle sobre tamanhos de buffer, deseja pular ou inspecionar linhas brutas, ou precisa de números de linha para relatório de erros. Lida com linhas até o tamanho do buffer configurado.

json.Decoder

Recomendado

Decodificador JSON de streaming que lê diretamente de um io.Reader. Mais eficiente que Scanner + Unmarshal porque evita copiar dados. Ideal para arquivos grandes e streams de rede. Lida com valores JSON consecutivos automaticamente sem necessidade de divisão explícita por nova linha.

json.Encoder

Escrita

Codificador JSON de streaming que escreve em um io.Writer. Adiciona automaticamente uma nova linha após cada chamada Encode, tornando-o perfeito para saída JSONL. Combine com bufio.Writer para escrita de alto desempenho. Suporta configuração de SetEscapeHTML e SetIndent.

Experimente Nossas Ferramentas JSONL Gratuitas

Não quer escrever código? Use nossas ferramentas online gratuitas para visualizar, validar e converter arquivos JSONL diretamente no seu navegador.

Trabalhe com Arquivos JSONL Online

Visualize, valide e converta arquivos JSONL de até 1GB diretamente no seu navegador. Sem uploads necessários, 100% privado.

Perguntas Frequentes

JSONL em Go — bufio.Scanner, json.Decoder e Concorrência ...