Dois irmãos acrobatas: Unmarshal e Decode
Se você programa em Go e trabalha com JSON, mais cedo ou mais tarde se depara com a escolha: usar json.Unmarshal ou json.NewDecoder.Decode? A pergunta é tão popular que no Stack Overflow já acumulou milhares de visualizações e dezenas de votos. Mas a resposta que você encontra lá geralmente se resume a "depende da tarefa".
Hoje não vamos dar apenas uma resposta abstrata. Vamos fazer nossa própria investigação: analisar os bastidores de ambos os métodos, escrever benchmarks, mostrar exemplos reais de código e definir regras claras sobre quando usar cada um. Aperte os cintos — vai esquentar!
Resumindo: o que são Unmarshal e Decode?
Ambas as ferramentas fazem a mesma coisa — transformam JSON em uma estrutura Go. Mas fazem isso de maneiras diferentes.
json.Unmarshal— recebe como entrada[]bytee um ponteiro para a estrutura. Tudo de uma vez: os bytes estão lá, a estrutura está pronta, vamos nessa. Funciona "diretamente" — desserializa todo o array de dados na memória.json.NewDecoder.Decode— trabalha com fluxo (io.Reader). Você cria um decoder, passa um reader (por exemplo,os.Fileouhttp.Response.Body) e chamaDecode. O decoder lê os dados em partes, sem carregar tudo na memória de uma vez.
Parece óbvia a diferença: para dados grandes — Decoder, para dados pequenos — Unmarshal. Mas o diabo, como sempre, está nos detalhes.
Os bastidores: como funcionam internamente
json.Unmarshal — tiro de precisão
Unmarshal é uma função que:
- Recebe um slice de bytes.
- Aloca memória para a estrutura resultante.
- Em uma única passada, faz o parsing de todo o JSON e preenche a estrutura.
Tudo simples e elegante. Mas há um detalhe: se você passar um JSON grande para Unmarshal (por exemplo, 100 MB), primeiro você precisa ler esses 100 MB para a memória. Depois, Unmarshal alocará mais memória para a estrutura. Resultado: consumo duplo de RAM.
json.NewDecoder.Decode — o encanador
O Decoder funciona de forma diferente:
- Cria-se um novo decoder que envolve o
io.Reader. - O decoder lê os dados em buffers (por padrão, 4 KB).
- À medida que lê, ele faz o parsing do JSON token por token e preenche a estrutura.
A vantagem é que você não armazena todo o JSON na memória. A desvantagem é um pouco mais de overhead no gerenciamento do buffer e na leitura em fluxo.
Experimento: benchmarks não mentem
Vamos verificar a teoria na prática. Vamos escrever um benchmark simples para três cenários:
- JSON pequeno (1 KB)
- JSON médio (100 KB)
- JSON grande (10 MB)
package main
import ( "bytes" "encoding/json" "testing")
type Data struct { ID int `json:"id"` Name string `json:"name"` Items []int `json:"items"`}
func generateJSON(size int) []byte { data := Data{ ID: 1, Name: "test", Items: make([]int, size), } for i := 0; i < size; i++ { data.Items[i] = i } b, _ := json.Marshal(data) return b}
func BenchmarkUnmarshal(b *testing.B) { jsonData := generateJSON(1000) // 1 KB var result Data for i := 0; i < b.N; i++ { json.Unmarshal(jsonData, &result) }}
func BenchmarkDecoder(b *testing.B) { jsonData := generateJSON(1000) // 1 KB var result Data for i := 0; i < b.N; i++ { reader := bytes.NewReader(jsonData) json.NewDecoder(reader).Decode(&result) }}Resultados (no meu notebook, Go 1.22):