Trabalhando com JSON em Go: Da Biblioteca Padrão a Alta Performance
Quando usar Marshal e Unmarshal na memória e quando usar Decoder e Encoder em streams, com foco em performance no encoding/json.
Publicada em · 7 min de leitura
Trabalhando com JSON em Go: Da Biblioteca Padrão a Alta Performance
O formato JSON (JavaScript Object Notation) é a língua franca da comunicação entre microsserviços modernos, APIs RESTful e interfaces frontend. No ecossistema Go, a biblioteca padrão encoding/json oferece um mecanismo declarativo, seguro e baseado em reflexão para serialização (marshaling) e desserialização (unmarshaling).
No entanto, trabalhar com JSON de forma profissional em ambientes corporativos exige ir muito além de um simples json.Unmarshal. É imperativo dominar streaming de baixo consumo de memória, proteção contra ataques de injeção de parâmetros via campos desconhecidos, precisão numérica de 64 bits para clientes JavaScript, e o uso de json.RawMessage para manipular payloads heterogêneos sem perder a segurança do sistema de tipos.
1. Struct Tags e Controle Granular de Mapeamento
Em Go, apenas campos exportados (iniciados com letra maiúscula) são visíveis para o pacote encoding/json. As struct tags definem o contrato de mapeamento entre os identificadores do Go e as chaves JSON.
As Tags Essenciais do Sênior
| Tag | Função Prática | Exemplo |
|---|---|---|
json:"nome" | Renomeia o campo no JSON (geralmente snake_case ou camelCase). | Title string json:“title”“ |
json:"-" | Ignora completamente o campo (senhas, segredos internos). | Password string json:”-”“ |
json:",omitempty" | Omite a chave se o valor for o zero-value do tipo (0, "", false, nil). | Bio string json:“bio,omitempty”“ |
json:"id,string" | Converte o número Go em string no JSON para evitar perda de precisão no JS. | ID int64 json:“id,string”“ |
A Armadilha de 64-bit no JavaScript
O JavaScript utiliza IEEE 754 de precisão dupla para números, suportando inteiros seguros apenas até $2^{53} - 1$ (9.007.199.254.740.991). IDs gerados por Twitter Snowflake ou chaves primárias BIGINT do PostgreSQL (int64 em Go) têm até 64 bits ($2^{63} - 1$). Se você serializar um int64 como número puro, o frontend do navegador irá corromper os últimos dígitos.
A Sutileza do omitempty com Tipos Primitivos
Se um usuário enviar {"tax": 0} para um campo com json:"tax,omitempty", o valor 0 será interpretado como zero-value e omitido na serialização! Para permitir valores zero legítimos mantendo a omissão em caso de ausência, use ponteiros:
type Order struct {
ID int64 `json:"id,string"`
Discount *float64 `json:"discount,omitempty"` // Se nil, omite. Se &0.0, serializa "discount": 0
}
2. Buffering vs Streaming: json.Unmarshal vs json.NewDecoder
Em servidores HTTP de alta concorrência, a forma como você consome o corpo da requisição dita a estabilidade da memória RAM.
O Anti-Pattern do Buffer Intermediário
// ❌ ANTI-PATTERN: Aloca o payload duas vezes na memória
bodyBytes, err := io.ReadAll(r.Body) // 1ª alocação: slice completo na Heap
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var req CreateUserRequest
err = json.Unmarshal(bodyBytes, &req) // 2ª alocação: parsing do slice
O Padrão Sênior: Streaming Direto
json.NewDecoder consome dados diretamente do socket TCP (r.Body) em chunks, preenchendo a struct à medida que lê os bytes sem criar um buffer intermediário gigante na Heap:
// ✅ PADRÃO RECOMENDADO: Zero buffer intermediário
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "JSON inválido", http.StatusBadRequest)
return
}
3. Segurança em Produção: DisallowUnknownFields
Por padrão, o encoding/json descarta silenciosamente qualquer campo presente no JSON que não exista na struct de destino. Isso cria vulnerabilidades perigosas:
- Ataques de Mass Assignment / Parameter Tampering: Clientes podem injetar chaves que passam despercebidas.
- Bugs Silenciosos de Clientes: Se o cliente errar a digitação (
"emial"em vez de"email"), o campo ficará nulo sem que a API alerte sobre o erro.
Decodificador Estrito
Configure o decoder para rejeitar payloads com campos extras:
package httphelper
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
func DecodeStrictJSON(w http.ResponseWriter, r *http.Request, dst any) error {
// Limita o tamanho do payload a 1 Megabyte para evitar ataques de exaustão de memória
r.Body = http.MaxBytesReader(w, r.Body, 1024*1024)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // Rejeita propriedades desconhecidas
if err := dec.Decode(dst); err != nil {
var unmarshalErr *json.UnmarshalTypeError
if errors.As(err, &unmarshalErr) {
return fmt.Errorf("campo %q possui tipo inválido (esperado %s)", unmarshalErr.Field, unmarshalErr.Type)
}
return fmt.Errorf("payload inválido: %w", err)
}
// Garante que não há mais de um objeto JSON colado na mesma requisição
if dec.More() {
return errors.New("o corpo contém dados excedentes após o objeto JSON principal")
}
return nil
}
4. Payloads Dinâmicos e Polimórficos com json.RawMessage
Em arquiteturas orientadas a eventos ou sistemas de webhook (como Stripe ou GitHub), uma mensagem possui um cabeçalho padrão, mas seu conteúdo (payload / data) varia radicalmente dependendo do evento.
O erro comum é usar map[string]any, o que remove toda a segurança de tipos e exige dezenas de asserções de tipo inseguras.
A solução canônica em Go é json.RawMessage: ela preserva os bytes brutos do JSON durante o primeiro parsing, permitindo deserializar o conteúdo específico somente após inspecionar o tipo do evento.
package events
import (
"encoding/json"
"fmt"
)
type EventEnvelope struct {
ID string `json:"id"`
Type string `json:"type"` // Ex: "USER_REGISTERED" ou "PAYMENT_CONFIRMED"
CreatedAt string `json:"created_at"`
Data json.RawMessage `json:"data"` // Bytes brutos preservados!
}
type UserRegisteredPayload struct {
UserID string `json:"user_id"`
Email string `json:"email"`
}
type PaymentConfirmedPayload struct {
TransactionID string `json:"tx_id"`
Amount float64 `json:"amount"`
}
func ProcessEvent(eventBytes []byte) error {
var env EventEnvelope
if err := json.Unmarshal(eventBytes, &env); err != nil {
return fmt.Errorf("falha ao ler envelope: %w", err)
}
switch env.Type {
case "USER_REGISTERED":
var userPayload UserRegisteredPayload
if err := json.Unmarshal(env.Data, &userPayload); err != nil {
return fmt.Errorf("erro no payload de registro: %w", err)
}
fmt.Printf("Novo usuário registrado: %s\n", userPayload.Email)
case "PAYMENT_CONFIRMED":
var payPayload PaymentConfirmedPayload
if err := json.Unmarshal(env.Data, &payPayload); err != nil {
return fmt.Errorf("erro no payload de pagamento: %w", err)
}
fmt.Printf("Pagamento confirmado: $%.2f\n", payPayload.Amount)
default:
return fmt.Errorf("tipo de evento desconhecido: %s", env.Type)
}
return nil
}
5. Serialização Customizada: json.Marshaler e json.Unmarshaler
Quando você precisa controlar a representação exata de um tipo (ex: datas no formato YYYY-MM-DD, máscaras de CPF/cartão, ou enums fortemente tipados), implemente as interfaces nativas:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Exemplo: Formato de Data Exclusivamente YYYY-MM-DD
O tipo padrão time.Time serializa em formato RFC3339 completo (2026-10-20T14:32:00Z). Para expor apenas a data sem componente de hora:
package customtypes
import (
"fmt"
"strings"
"time"
)
type DateOnly time.Time
// MarshalJSON implementa json.Marshaler (receptor por valor)
func (d DateOnly) MarshalJSON() ([]byte, error) {
formatted := fmt.Sprintf("%q", time.Time(d).Format("2006-01-02"))
return []byte(formatted), nil
}
// UnmarshalJSON implementa json.Unmarshaler (receptor por PONTEIRO)
func (d *DateOnly) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
if s == "null" || s == "" {
return nil
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return fmt.Errorf("data deve estar no formato AAAA-MM-DD: %w", err)
}
*d = DateOnly(t)
return nil
}
6. A Armadilha do float64 e json.Number
Quando um payload contém números genéricos e é deserializado em um map[string]any ou any, o Go converte todos os números para float64. Isso pode causar perda de precisão e conversões indesejadas (um inteiro 1 vira 1.0).
Para preservar números exatamente como foram transmitidos, utilize o método UseNumber():
dec := json.NewDecoder(r.Body)
dec.UseNumber()
var genericData map[string]any
if err := dec.Decode(&genericData); err != nil {
return err
}
// O campo numérico agora é do tipo json.Number
if num, ok := genericData["counter"].(json.Number); ok {
intVal, err := num.Int64() // Lê diretamente como int64
floatVal, _ := num.Float64() // Ou como float64 se necessário
_ = intVal
_ = floatVal
}
7. Desempenho e Alternativas à Standard Library
A biblioteca padrão encoding/json prioriza robustez, compatibilidade e ausência de dependências CGo/unsafe, utilizando reflexão em tempo de execução. Para 95% dos serviços, ela é mais rápida que a latência de rede/banco.
No entanto, para gateways de alta volumetria que processam centenas de milhares de requisições por segundo:
| Biblioteca | Abordagem | Vantagens | Desvantagens |
|---|---|---|---|
encoding/json (Stdlib) | Reflexão nativa | Zero dependências, padrão universal, máxima segurança. | Mais lenta em payloads gigantescos. |
bytedance/sonic | JIT Assembler x86/ARM | Até 4x mais rápida que a stdlib. | Depende de código assembly específico de arquitetura. |
json-iterator/go | Drop-in replacement | 2-3x mais rápido, compatibilidade 100% com API stdlib. | Complexidade interna de reflexão. |
mailru/easyjson | Geração de código estático | Zero reflexão em runtime, altíssima velocidade. | Requer etapa de build (go generate). |
Recomendação de Arquitetura: Comece sempre com encoding/json nativo com json.NewDecoder. Apenas migre para sonic ou easyjson se o profiling de CPU (pprof) comprovar que o gargalo do sistema está na serialização.
8. Checklist de Produção
- Utilizar
json.NewDecoder(r.Body)em vez deio.ReadAll+json.Unmarshal. - Envolver
r.Bodycomhttp.MaxBytesReaderpara bloquear Denial of Service por JSON gigante. - Ativar
dec.DisallowUnknownFields()em endpoints de entrada para garantir integridade do contrato. - Aplicar
json:"id,string"em identificadoresint64consumidos por navegadores web. - Utilizar
json.RawMessagepara eventos heterogêneos mantendo forte tipagem. - Implementar
UnmarshalJSONcom receptor de ponteiro (*T). - Usar ponteiros para tipos primitivos (
*bool,*float64) quando0oufalseforem valores válidos comomitempty.
Links Relacionados
- Manipulação de Arquivos e IO: Streaming com
io.Readereio.Writer. - Cliente HTTP Profissional: Consumo e envio de JSON em clientes HTTP de alta performance.
- Servidor HTTP Nativo Moderno: Estruturando handlers com decodificação padronizada.