Pular para o conteúdo

Trilha de Go · parte 7 de 37

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

Tamanho da fonte:
Largura da coluna:

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.

Anatomia de Struct Tags JSON
figura 1 · Anatomia de Struct Tags JSON

As Tags Essenciais do Sênior

TagFunção PráticaExemplo
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.

Buffer vs Streaming JSON
figura 2 · Buffer vs Streaming JSON

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:

  1. Ataques de Mass Assignment / Parameter Tampering: Clientes podem injetar chaves que passam despercebidas.
  2. 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:

BibliotecaAbordagemVantagensDesvantagens
encoding/json (Stdlib)Reflexão nativaZero dependências, padrão universal, máxima segurança.Mais lenta em payloads gigantescos.
bytedance/sonicJIT Assembler x86/ARMAté 4x mais rápida que a stdlib.Depende de código assembly específico de arquitetura.
json-iterator/goDrop-in replacement2-3x mais rápido, compatibilidade 100% com API stdlib.Complexidade interna de reflexão.
mailru/easyjsonGeração de código estáticoZero 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 de io.ReadAll + json.Unmarshal.
  • Envolver r.Body com http.MaxBytesReader para bloquear Denial of Service por JSON gigante.
  • Ativar dec.DisallowUnknownFields() em endpoints de entrada para garantir integridade do contrato.
  • Aplicar json:"id,string" em identificadores int64 consumidos por navegadores web.
  • Utilizar json.RawMessage para eventos heterogêneos mantendo forte tipagem.
  • Implementar UnmarshalJSON com receptor de ponteiro (*T).
  • Usar ponteiros para tipos primitivos (*bool, *float64) quando 0 ou false forem valores válidos com omitempty.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?