JSON Formatter Go — Guia do MarshalIndent()

·Systems Engineer·Revisado porTobias Müller·Publicado

Use o Formatador e Embelezador JSON gratuito diretamente no seu navegador — sem instalação.

Experimentar Formatador e Embelezador JSON online →

Quando estou trabalhando em um microsserviço Go e preciso inspecionar uma resposta de API ou um arquivo de configuração, o JSON compacto é o primeiro obstáculo — uma única linha com centenas de campos aninhados não me diz quase nada à primeira vista. Para formatar JSON em Go, a biblioteca padrão oferece tudo que você precisa: json.MarshalIndent está embutido em encoding/json, vem com cada instalação do Go e não requer dependências externas. Este guia cobre o quadro completo: struct tags, implementações personalizadas de MarshalJSON() , json.Indent para reformatar bytes brutos, streaming de arquivos grandes com json.Decoder, e quando recorrer a go-json para caminhos de alto desempenho, além de comandos de linha única para formatar rapidamente no terminal. Todos os exemplos usam Go 1.21+.

  • json.MarshalIndent(v, "", "\t") é da biblioteca padrão — zero dependências, incluído em cada instalação do Go.
  • Struct tags json:"field_name,omitempty" controlam as chaves de serialização e omitem campos com valor zero.
  • Implemente MarshalJSON() em qualquer tipo para controlar completamente sua representação JSON.
  • json.Indent() reformata []byte já serializados sem re-parsear a struct — mais rápido para bytes brutos.
  • Para arquivos grandes (>100 MB): use json.Decoder com Token() para streaming sem carregar tudo na memória.
  • go-json é um substituto direto 3–5× mais rápido que encoding/json para APIs de alto throughput.

O que é formatação de JSON?

A formatação de JSON — também chamada de pretty-printing — transforma uma string JSON compacta e minificada em um layout legível com indentação e quebras de linha consistentes. Os dados subjacentes são idênticos; apenas os espaços em branco mudam. O JSON compacto é ideal para transferência de rede onde cada byte importa; o JSON formatado é ideal para depuração, revisão de código, inspeção de logs e edição de arquivos de configuração. O pacote encoding/json do Go lida com ambos os modos com uma única chamada de função — alterne entre saída compacta e indentada escolhendo entre json.Marshal e json.MarshalIndent.

Before · json
After · json
{"service":"payments","port":8443,"workers":4}
{
	"service": "payments",
	"port": 8443,
	"workers": 4
}

json.MarshalIndent() — A abordagem da biblioteca padrão

json.MarshalIndent faz parte do pacote encoding/json , que integra a biblioteca padrão do Go — nenhum go get necessário. Sua assinatura é MarshalIndent(v any, prefix, indent string) ([]byte, error): a string prefix é anteposta a cada linha de saída (quase sempre deixada vazia), e indent é repetida uma vez por nível de aninhamento. Passe "\t" para tabulações ou " " para dois espaços.

Go — exemplo mínimo funcional
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type ServiceConfig struct {
	Host       string   `json:"host"`
	Port       int      `json:"port"`
	Workers    int      `json:"workers"`
	TLSEnabled bool     `json:"tls_enabled"`
	AllowedIPs []string `json:"allowed_ips"`
}

func main() {
	cfg := ServiceConfig{
		Host:       "payments-api.internal",
		Port:       8443,
		Workers:    8,
		TLSEnabled: true,
		AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
	}

	data, err := json.MarshalIndent(cfg, "", "	")
	if err != nil {
		log.Fatalf("marshal config: %v", err)
	}
	fmt.Println(string(data))
}
// {
// 	"host": "payments-api.internal",
// 	"port": 8443,
// 	"workers": 8,
// 	"tls_enabled": true,
// 	"allowed_ips": [
// 		"10.0.0.0/8",
// 		"172.16.0.0/12"
// 	]
// }

A escolha entre tabulações e espaços é principalmente uma convenção de equipe. Muitos projetos Go preferem tabulações porque gofmt (que formata código-fonte Go) as utiliza. Indentação de dois ou quatro espaços é comum quando o JSON é destinado a um consumidor JavaScript ou Python. Aqui está a mesma struct com ambos os estilos de indentação lado a lado:

Go — tabulações vs espaços
// Indentação com tabulações — preferida em ferramentas nativas do Go
data, _ := json.MarshalIndent(cfg, "", "	")
// {
// 	"host": "payments-api.internal",
// 	"port": 8443
// }

// Indentação com dois espaços — comum para APIs com consumidores JS/Python
data, _ = json.MarshalIndent(cfg, "", "  ")
// {
//   "host": "payments-api.internal",
//   "port": 8443
// }

// Indentação com quatro espaços
data, _ = json.MarshalIndent(cfg, "", "    ")
// {
//     "host": "payments-api.internal",
//     "port": 8443
// }
Nota:Use json.Marshal(v) quando precisar de saída compacta — payloads de rede, valores em cache ou qualquer caminho onde o tamanho em bytes importa. Aceita o mesmo argumento de valor e tem a mesma semântica de erro, mas produz JSON em uma única linha sem espaços em branco.

Struct Tags — Controlando nomes de campos e omitempty

Struct tags do Go são literais de string colocadas após declarações de campo que informam aencoding/jsoncomo serializar cada campo. Existem três diretivas principais: json:"name" renomeia o campo na saída, omitempty omite o campo quando contém o valor zero do seu tipo, e json:"-" exclui o campo completamente — útil para senhas, identificadores internos ou campos que nunca devem cruzar o limite do serviço.

Go — struct tags para uma resposta de API
type UserProfile struct {
	ID          string  `json:"id"`
	Email       string  `json:"email"`
	DisplayName string  `json:"display_name,omitempty"`  // omitir se string vazia
	AvatarURL   *string `json:"avatar_url,omitempty"`    // omitir se ponteiro nil
	IsAdmin     bool    `json:"is_admin,omitempty"`      // omitir se false
	passwordHash string                                   // não exportado — excluído automaticamente
}

// Usuário com todos os campos opcionais preenchidos
full := UserProfile{
	ID: "usr_7b3c", Email: "j.silva@exemplo.com",
	DisplayName: "João Silva", IsAdmin: true,
}
// {
//   "id": "usr_7b3c",
//   "email": "j.silva@exemplo.com",
//   "display_name": "João Silva",
//   "is_admin": true
// }

// Usuário sem campos opcionais — são omitidos por completo
minimal := UserProfile{ID: "usr_2a91", Email: "c.rocha@exemplo.com"}
// {
//   "id": "usr_2a91",
//   "email": "c.rocha@exemplo.com"
// }

A tag json:"-" é a escolha certa para campos que devem ser excluídos incondicionalmente independentemente do seu valor — tipicamente segredos, campos de rastreamento interno ou dados corretos em memória que nunca devem ser serializados para qualquer sistema externo.

Go — excluir campos sensíveis
type AuthToken struct {
	TokenID      string `json:"token_id"`
	Subject      string `json:"sub"`
	IssuedAt     int64  `json:"iat"`
	ExpiresAt    int64  `json:"exp"`
	SigningKey    []byte `json:"-"`   // nunca serializado
	RefreshToken string `json:"-"`   // nunca serializado
}

tok := AuthToken{
	TokenID: "tok_8f2a", Subject: "usr_7b3c",
	IssuedAt: 1741614120, ExpiresAt: 1741617720,
	SigningKey: []byte("segredo"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", "  ")
// {
//   "token_id": "tok_8f2a",
//   "sub": "usr_7b3c",
//   "iat": 1741614120,
//   "exp": 1741617720
// }
// SigningKey e RefreshToken nunca aparecem
Nota:Campos de struct não exportados (em minúsculo) são sempre excluídos por encoding/json independentemente de qualquer tag. Não é necessário adicionar json:"-" a campos não exportados — a exclusão é automática e não pode ser substituída.

MarshalJSON() personalizado — Tratando tipos não padrão

Qualquer tipo Go pode implementar a interface json.Marshaler definindo um método MarshalJSON() ([]byte, error). Quando encoding/json encontra tal tipo, chama o método em vez de sua serialização padrão baseada em reflexão. Este é o padrão Go canônico para tipos de domínio que precisam de uma representação de rede específica — valores monetários, enums de status, formatos de data personalizados ou qualquer tipo que armazene dados de forma diferente de como deve ser serializado.

Tipo personalizado — Money com conversão de centavos para decimal

Go — MarshalJSON personalizado para Money
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Money struct {
	Amount   int64  // armazenado em centavos para evitar imprecisão de ponto flutuante
	Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		Amount   float64 `json:"amount"`
		Currency string  `json:"currency"`
		Display  string  `json:"display"`
	}{
		Amount:   float64(m.Amount) / 100,
		Currency: m.Currency,
		Display:  fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
	})
}

type Invoice struct {
	ID       string `json:"id"`
	Subtotal Money  `json:"subtotal"`
	Tax      Money  `json:"tax"`
	Total    Money  `json:"total"`
}

func main() {
	inv := Invoice{
		ID:       "inv_9a2f91bc",
		Subtotal: Money{Amount: 19900, Currency: "BRL"},
		Tax:      Money{Amount: 1592, Currency: "BRL"},
		Total:    Money{Amount: 21492, Currency: "BRL"},
	}
	data, err := json.MarshalIndent(inv, "", "  ")
	if err != nil {
		log.Fatalf("marshal invoice: %v", err)
	}
	fmt.Println(string(data))
}
// {
//   "id": "inv_9a2f91bc",
//   "subtotal": { "amount": 199, "currency": "BRL", "display": "BRL 199.00" },
//   "tax":      { "amount": 15.92, "currency": "BRL", "display": "BRL 15.92" },
//   "total":    { "amount": 214.92, "currency": "BRL", "display": "BRL 214.92" }
// }

Enum de status — Representação como string

Go — MarshalJSON personalizado para enum de status
type OrderStatus int

const (
	StatusPending OrderStatus = iota
	StatusPaid
	StatusShipped
	StatusCancelled
)

var orderStatusNames = map[OrderStatus]string{
	StatusPending:   "pendente",
	StatusPaid:      "pago",
	StatusShipped:   "enviado",
	StatusCancelled: "cancelado",
}

func (s OrderStatus) MarshalJSON() ([]byte, error) {
	name, ok := orderStatusNames[s]
	if !ok {
		return nil, fmt.Errorf("status de pedido desconhecido: %d", s)
	}
	return json.Marshal(name)
}

type Order struct {
	ID     string      `json:"id"`
	Status OrderStatus `json:"status"`
}

o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", "  ")
// {
//   "id": "ord_3c7f",
//   "status": "enviado"
// }
Nota:Implemente sempre MarshalJSON() e UnmarshalJSON() juntos. Se você implementar apenas a serialização, fazer um ciclo completo do tipo por JSON (serializar → armazenar → desserializar) perderá estrutura silenciosamente ou retornará o tipo errado. O par forma um contrato que garante que o tipo pode sobreviver a um ciclo completo de JSON.

UUID — Serializar como string

A biblioteca padrão do Go não tem tipo UUID. A escolha mais comum é github.com/google/uuid, que já implementa MarshalJSON() e serializa como uma string RFC 4122 entre aspas. Se você usar um [16]byte bruto ou um tipo ID personalizado, implemente a interface você mesmo para evitar blobs binários codificados em base64 na sua saída JSON.

Go 1.21+ — serialização de UUID
import (
    "encoding/json"
    "fmt"

    "github.com/google/uuid"
)

type AuditEvent struct {
    EventID   uuid.UUID `json:"event_id"`   // serializa como "550e8400-e29b-41d4-a716-446655440000"
    SessionID uuid.UUID `json:"session_id"`
    Action    string    `json:"action"`
    ActorID   string    `json:"actor_id"`
    OccuredAt string    `json:"occurred_at"`
}

event := AuditEvent{
    EventID:   uuid.New(),
    SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
    Action:    "usuario.senha_alterada",
    ActorID:   "usr_7f3a91bc",
    OccuredAt: "2026-03-10T14:22:00Z",
}

data, _ := json.MarshalIndent(event, "", "  ")
fmt.Println(string(data))
// {
//   "event_id": "a4b2c1d0-...",
//   "session_id": "550e8400-e29b-41d4-a716-446655440000",
//   "action": "usuario.senha_alterada",
//   "actor_id": "usr_7f3a91bc",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }
Nota:Se você armazenar UUIDs como [16]byte sem um tipo personalizado, encoding/json os codifica como string base64 — ex.: "VQ6EAOKbQdSnFkRmVUQAAA==". Use sempre um tipo UUID adequado ou implemente MarshalJSON() para emitir o formato canônico de string com hífens.

Referência de parâmetros de json.MarshalIndent()

A assinatura da função é func MarshalIndent(v any, prefix, indent string) ([]byte, error). Tanto prefix quanto indent são literais de string — não há atalho numérico como o indent=4 do Python.

Parâmetro
Tipo
Descrição
v
any
O valor a serializar — struct, map, slice ou primitivo
prefix
string
String anteposta a cada linha de saída (normalmente "")
indent
string
String usada em cada nível de indentação ("\t" ou " ")

Opções comuns de struct tags:

Tag
Efeito
json:"name"
Renomeia o campo para name na saída JSON
json:"name,omitempty"
Renomeia + omite se o valor for zero (nil, "", 0, false)
json:"-"
Sempre exclui este campo da saída JSON
json:",string"
Codifica número ou bool como string JSON

json.Indent() — Reformatar bytes JSON existentes

Quando você já tem um []byte de JSON — por exemplo do corpo de uma resposta HTTP, uma coluna jsonb do Postgres, ou um arquivo lido com os.ReadFile — não precisa definir uma struct nem fazer unmarshal antes de formatar. json.Indent reformata diretamente os bytes brutos escrevendo a saída indentada em um bytes.Buffer.

Go — json.Indent sobre bytes brutos
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	// Simulando um payload JSON bruto de um serviço upstream
	raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)

	var buf bytes.Buffer
	if err := json.Indent(&buf, raw, "", "	"); err != nil {
		log.Fatalf("indent: %v", err)
	}
	fmt.Println(buf.String())
}
// {
// 	"trace_id": "tr_9a2f",
// 	"service": "checkout",
// 	"latency_ms": 342,
// 	"error": null
// }

Um padrão que uso frequentemente em microsserviços é chamar json.Indent antes de escrever em logs estruturados — adiciona overhead mínimo e torna as entradas de log muito mais fáceis de ler durante um incidente. A função é especialmente útil para registrar respostas HTTP, formatar strings JSON armazenadas e pipelines de formato-na-leitura onde a definição da struct não está disponível.

Go — json.Indent para logging de depuração
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
	var pretty bytes.Buffer
	if err := json.Indent(&pretty, body, "", "  "); err != nil {
		// Corpo não é JSON válido — registrar bruto
		logger.Debug("resposta upstream", "status", statusCode, "body", string(body))
		return
	}
	logger.Debug("resposta upstream", "status", statusCode, "body", pretty.String())
}
Aviso:json.Indent NÃO valida completamente o JSON além do que é estruturalmente necessário para inserir espaços em branco. Para validação sintática completa, chame json.Valid(data) primeiro e trate o caso false antes de tentar a indentação.

Formatar JSON de um arquivo e de uma resposta HTTP

Dois dos cenários mais comuns em serviços Go são formatar JSON lido de um arquivo em disco (arquivos de configuração, dados de fixtures, seeds de migração) e formatar corpos de resposta HTTP para logging de depuração ou asserções em testes. Ambos seguem o mesmo padrão: ler bytes, chamar json.Indent ou fazer unmarshal e depois json.MarshalIndent, escrever de volta ou registrar.

Ler arquivo → Formatar → Escrever de volta

Go — formatar arquivo JSON no lugar
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"os"
)

func formatJSONFile(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("ler %s: %w", path, err)
	}

	if !json.Valid(data) {
		return fmt.Errorf("JSON inválido em %s", path)
	}

	var buf bytes.Buffer
	if err := json.Indent(&buf, data, "", "	"); err != nil {
		return fmt.Errorf("indent %s: %w", path, err)
	}

	if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
		return fmt.Errorf("escrever %s: %w", path, err)
	}
	return nil
}

func main() {
	if err := formatJSONFile("config/database.json"); err != nil {
		log.Fatalf("formatar config: %v", err)
	}
	fmt.Println("config/database.json formatado com sucesso")
}

Resposta HTTP → Decodificar → Formatar para logging de depuração

Go — formatar resposta HTTP para log de depuração
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

type HealthResponse struct {
	Status    string            `json:"status"`
	Version   string            `json:"version"`
	Checks    map[string]string `json:"checks"`
	UptimeSec int64             `json:"uptime_seconds"`
}

func main() {
	resp, err := http.Get("https://api.payments.internal/v2/health")
	if err != nil {
		log.Fatalf("health check: %v", err)
	}
	defer resp.Body.Close()

	var result HealthResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		log.Fatalf("decodificar resposta health: %v", err)
	}

	pretty, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("marshal resposta health: %v", err)
	}
	fmt.Printf("Health check (%d):
%s
", resp.StatusCode, string(pretty))
}
// Health check (200):
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "checks": {
//     "database": "ok",
//     "cache": "ok",
//     "queue": "degraded"
//   },
//   "uptime_seconds": 172800
// }

Corpo de resposta bruto → json.Indent (sem struct necessária)

Go — formatar corpo HTTP bruto com io.ReadAll
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	resp, err := http.Get("https://api.payments.internal/v2/health")
	if err != nil {
		log.Fatalf("request: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("ler corpo: %v", err)
	}

	var buf bytes.Buffer
	if err := json.Indent(&buf, body, "", "  "); err != nil {
		log.Fatalf("indent: %v", err)
	}
	fmt.Println(buf.String())
}

Exibir JSON formatado de uma resposta HTTP em Go

As duas abordagens acima cobrem os casos mais comuns: decodificar em uma struct tipada e depois chamar json.MarshalIndent (melhor quando você precisa validar ou inspecionar campos), ou ler os bytes brutos do corpo com io.ReadAll e chamar diretamente json.Indent (melhor para logging de depuração rápida quando você não tem uma definição de struct à mão). A abordagem de bytes brutos é mais simples, mas não fornece segurança de tipos Go nem acesso a campos — é puramente uma transformação de exibição. Ambas as abordagens tratam corpos de resposta grandes corretamente, desde que o corpo completo caiba na memória.

Go — dois padrões lado a lado
// Padrão A: decodificação tipada → MarshalIndent
// Use quando precisar inspecionar ou validar campos específicos
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
pretty, _ := json.MarshalIndent(result, "", "  ")
fmt.Println(string(pretty))

// Padrão B: bytes brutos → json.Indent
// Use para logging de depuração rápida — sem definição de struct necessária
body, _ := io.ReadAll(resp.Body)
var buf bytes.Buffer
json.Indent(&buf, body, "", "  ")
fmt.Println(buf.String())

Formatação de JSON por linha de comando em projetos Go

Às vezes você precisa formatar um payload JSON direto no terminal sem escrever um programa Go. Estes comandos de linha única são os que tenho na memória muscular durante o desenvolvimento e a resposta a incidentes.

bash — passar JSON para o formatador integrado do Python
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — passar para jq para formatação e filtragem completas
# Apenas formatar
cat api-response.json | jq .

# Extrair um campo aninhado
cat api-response.json | jq '.checks.database'

# Filtrar um array
cat audit-log.json | jq '.[] | select(.severity == "error")'
bash — passar para um main.go mínimo via stdin
# main.go: lê stdin, formata, escreve stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
    b,_:=io.ReadAll(os.Stdin)
    var buf bytes.Buffer
    json.Indent(&buf,b,"","  ")
    os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.go
Nota:gofmt formata código-fonte Go, não JSON. Não passe arquivos JSON por gofmt — ele produzirá um erro ou saída irreconhecível. Use jq . ou python3 -m json.tool para arquivos JSON.

Alternativa de alto desempenho — go-json

Para a grande maioria dos serviços Go, encoding/json é rápido o suficiente. Mas se a serialização JSON aparecer no seu profiler — comum em APIs REST de alto throughput ou serviços que emitem linhas de log estruturadas grandes em cada requisição — a biblioteca go-json é um substituto direto que é 3–5× mais rápido com superfície de API idêntica.

bash — instalar go-json
go get github.com/goccy/go-json
Go — substituir encoding/json por go-json com uma única mudança de import
package main

import (
	// Substitua isso:
	// "encoding/json"

	// Por isso — API idêntica, sem outras mudanças de código:
	json "github.com/goccy/go-json"

	"fmt"
	"log"
)

type AuditEvent struct {
	RequestID string `json:"request_id"`
	UserID    string `json:"user_id"`
	Action    string `json:"action"`
	ResourceID string `json:"resource_id"`
	IPAddress  string `json:"ip_address"`
	DurationMs int    `json:"duration_ms"`
}

func main() {
	event := AuditEvent{
		RequestID: "req_7d2e91", UserID: "usr_4421",
		Action: "fatura.download", ResourceID: "inv_9a2f",
		IPAddress: "203.0.113.45", DurationMs: 23,
	}
	data, err := json.MarshalIndent(event, "", "  ")
	if err != nil {
		log.Fatalf("marshal: %v", err)
	}
	fmt.Println(string(data))
	// A saída é idêntica a encoding/json — apenas a velocidade difere
}

github.com/bytedance/sonic é a biblioteca JSON mais rápida disponível para Go, mas funciona apenas em amd64 e arm64 (usa compilação JIT). Use go-json quando precisar de um substituto portável; recorra a sonic quando estiver em uma arquitetura conhecida e precisar de cada microssegundo em um caminho crítico.

Trabalhando com arquivos JSON grandes

Tanto json.MarshalIndent quanto json.Indent requerem que o payload completo esteja na memória. Para arquivos acima de 100 MB — exportações de dados, logs de auditoria, payloads de consumidores Kafka — use json.Decoder para fazer streaming da entrada e processar registros um por vez.

Streaming de um array JSON grande com json.Decoder

Go — stream de array JSON grande sem carregar na memória
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type AuditEvent struct {
	RequestID  string `json:"request_id"`
	UserID     string `json:"user_id"`
	Action     string `json:"action"`
	Severity   string `json:"severity"`
	DurationMs int    `json:"duration_ms"`
}

func processAuditLog(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("abrir %s: %w", path, err)
	}
	defer file.Close()

	dec := json.NewDecoder(file)

	// Ler o '[' de abertura
	if _, err := dec.Token(); err != nil {
		return fmt.Errorf("ler token de abertura: %w", err)
	}

	var processed int
	for dec.More() {
		var event AuditEvent
		if err := dec.Decode(&event); err != nil {
			return fmt.Errorf("decodificar evento %d: %w", processed, err)
		}
		// Processar um evento por vez — uso de memória constante
		if event.Severity == "error" {
			fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
		}
		processed++
	}
	fmt.Printf("Processados %d eventos de auditoria
", processed)
	return nil
}

func main() {
	if err := processAuditLog("audit-2026-03.json"); err != nil {
		log.Fatalf("processar log de auditoria: %v", err)
	}
}

NDJSON — Um objeto JSON por linha

Go — processar stream NDJSON linha por linha
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type LogLine struct {
	Timestamp  string `json:"ts"`
	Level      string `json:"level"`
	Service    string `json:"service"`
	Message    string `json:"msg"`
	TraceID    string `json:"trace_id"`
	DurationMs int    `json:"duration_ms,omitempty"`
}

func main() {
	file, err := os.Open("service-2026-03.ndjson")
	if err != nil {
		log.Fatalf("abrir log: %v", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB por linha

	for scanner.Scan() {
		var line LogLine
		if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
			continue // ignorar linhas malformadas
		}
		if line.Level == "error" {
			fmt.Printf("%s [%s] %s trace=%s
",
				line.Timestamp, line.Service, line.Message, line.TraceID)
		}
	}
	if err := scanner.Err(); err != nil {
		log.Fatalf("scan: %v", err)
	}
}
Nota:Mude para streaming quando o arquivo ultrapassar 100 MB ou ao processar streams ilimitados (consumidores Kafka, pipelines de log, leituras de objetos S3). Carregar um arquivo JSON de 500 MB com os.ReadFile alocará todo esse buffer no heap, gerará pressão no GC e pode causar OOM em containers com memória limitada.

Erros comuns

Ignorar o retorno de erro de MarshalIndent

Problema: Descartar o erro com _ significa que um valor não serializável (canal, função, número complexo) produz silenciosamente nil como saída ou causa um panic mais adiante ao chamar string(nil).

Solução: Sempre verifique o erro. Se você estiver serializando um tipo que sempre deveria ter sucesso, um log.Fatalf com panic é melhor do que perda silenciosa de dados.

Before · Go
After · Go
data, _ := json.MarshalIndent(payload, "", "  ")
fmt.Println(string(data)) // string vazia se o marshal falhou
data, err := json.MarshalIndent(payload, "", "  ")
if err != nil {
    log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))
Usar fmt.Println em vez de os.Stdout.Write para saída binária

Problema: fmt.Println(string(data)) adiciona um caractere de nova linha ao final do JSON, o que corrompe pipelines que tratam a saída como bytes brutos — por exemplo, ao passar para jq ou escrever em um protocolo binário.

Solução: Use os.Stdout.Write(data) para saída binária limpa. Se precisar de uma nova linha ao final para exibição humana, adicione-a explicitamente.

Before · Go
After · Go
data, _ := json.MarshalIndent(cfg, "", "  ")
fmt.Println(string(data)) // adiciona uma nova linha extra ao final
data, _ := json.MarshalIndent(cfg, "", "  ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // nova linha explícita apenas quando necessário
Esquecer omitempty em campos ponteiro

Problema: Sem omitempty, um ponteiro nil *string ou *int é serializado como "field": null. Isso expõe nomes de campos internos e pode quebrar validadores de esquema JSON estritos no lado do consumidor.

Solução: Adicione omitempty a campos ponteiro que você quer ausentes (não null) na saída. Um *T nil com omitempty não produz nenhuma chave no JSON.

Before · Go
After · Go
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg"`  // aparece como null quando nil
}
// {"event_id":"evt_3c7f","error_msg":null}
type WebhookPayload struct {
    EventID   string  `json:"event_id"`
    ErrorMsg  *string `json:"error_msg,omitempty"`  // omitido quando nil
}
// {"event_id":"evt_3c7f"}
Usar map[string]interface{} em vez de structs

Problema: Fazer unmarshal em map[string]any perde informação de tipos, requer type assertions manuais e produz ordem de chaves não determinística — tornando diffs de JSON e comparações de log mais difíceis.

Solução: Defina uma struct com json tags adequados. Structs são type-safe, serializam mais rápido, produzem ordem de campos determinística correspondendo à definição da struct e tornam o código autodocumentado.

Before · Go
After · Go
var result map[string]any
json.Unmarshal(body, &result)
port := result["port"].(float64) // type assertion necessária, causa panic se o tipo estiver errado
type ServiceStatus struct {
    Service string `json:"service"`
    Port    int    `json:"port"`
    Healthy bool   `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // tipado, seguro, rápido

encoding/json vs alternativas — Comparação rápida

Método
Saída formatada
JSON válido
Tipos personalizados
Streaming
Requer instalação
json.MarshalIndent
✓ via MarshalJSON
Não (stdlib)
json.Indent
N/A (apenas bytes)
Não (stdlib)
json.Encoder
✗ (compacto)
✓ via MarshalJSON
Não (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
Instalação no sistema

Use json.MarshalIndent para qualquer caso onde você controla a definição da struct e precisa de saída formatada — arquivos de configuração, logging de depuração, fixtures de testes e logging de respostas de API. Use json.Indent quando você já tem bytes brutos e apenas precisa adicionar espaços sem ida e volta por tipos Go. Mude para go-json ou sonic somente após o profiling confirmar que a serialização JSON é um gargalo mensurável — para a maioria dos serviços, a biblioteca padrão é mais do que suficiente.

Perguntas frequentes

Como exibo JSON formatado em Go?

Chame json.MarshalIndent(v, "", "\t") do pacote encoding/json — o segundo argumento é um prefixo por linha (normalmente vazio) e o terceiro é a indentação por nível. Passe "\t" para tabulações ou " " para dois espaços. Nenhuma biblioteca externa é necessária; encoding/json faz parte da biblioteca padrão do Go.

Go
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	config := map[string]any{
		"service": "payments-api",
		"port":    8443,
		"region":  "sa-east-1",
	}
	data, err := json.MarshalIndent(config, "", "	")
	if err != nil {
		log.Fatalf("marshal: %v", err)
	}
	fmt.Println(string(data))
	// {
	// 	"port": 8443,
	// 	"region": "sa-east-1",
	// 	"service": "payments-api"
	// }
}

Qual é a diferença entre json.Marshal e json.MarshalIndent?

json.Marshal produz JSON compacto em uma única linha sem espaços em branco — ideal para transferência de rede onde cada byte importa. json.MarshalIndent aceita dois parâmetros extras (prefix e indent) e produz saída indentada e legível. Ambas as funções aceitam os mesmos tipos de valor e retornam ([]byte, error). O único custo de MarshalIndent são alguns bytes a mais na saída e um mínimo de CPU extra para inserir os espaços.

Go
import "encoding/json"

type HealthCheck struct {
	Status  string `json:"status"`
	Version string `json:"version"`
	Uptime  int    `json:"uptime_seconds"`
}

h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}

compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}

pretty, _ := json.MarshalIndent(h, "", "  ")
// {
//   "status": "ok",
//   "version": "1.4.2",
//   "uptime_seconds": 86400
// }

Como formato um []byte de JSON sem fazer unmarshal da struct?

Use json.Indent(&buf, src, "", "\t"). Esta função recebe um []byte de JSON existente e escreve a versão indentada em um bytes.Buffer — sem definir uma struct, sem type assertion e sem ida e volta por tipos Go. É a opção mais rápida quando você já tem bytes JSON brutos, por exemplo do corpo de uma resposta HTTP ou de uma coluna de banco de dados.

Go
import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
)

raw := []byte(`{"endpoint":"/api/v2/faturas","page":1,"per_page":50,"total":312}`)

var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", "	"); err != nil {
	log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// 	"endpoint": "/api/v2/faturas",
// 	"page": 1,
// 	"per_page": 50,
// 	"total": 312
// }

Por que json.MarshalIndent retorna um erro?

encoding/json retorna um erro quando o valor não pode ser representado como JSON. As causas mais comuns são: serializar um canal, uma função ou um número complexo (esses tipos não têm equivalente JSON); uma struct que implementa MarshalJSON() e retorna um erro; ou um map com chaves que não são string. Importante: serializar uma struct com campo não exportado ou ponteiro nil NÃO causa erro — eles são simplesmente omitidos.

Go
import (
	"encoding/json"
	"fmt"
)

// Isso retornará um erro — canais não são serializáveis como JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", "	")
fmt.Println(err)
// json: unsupported type: chan int

// Isso está correto — campos ponteiro nil com omitempty são omitidos silenciosamente
type Profile struct {
	ID     string  `json:"id"`
	Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", "  ")
// {"id": "usr_7b3c"}  — avatar omitido, sem erro

Como excluo um campo da saída JSON em Go?

Existem três formas. Primeira, use o struct tag json:"-" — o campo sempre é excluído independentemente do seu valor. Segunda, use omitempty — o campo é excluído apenas quando contém o valor zero do seu tipo (ponteiro nil, string vazia, 0, false). Terceira, campos não exportados (em minúsculo) são excluídos automaticamente por encoding/json sem necessidade de tag.

Go
type PaymentMethod struct {
	ID           string  `json:"id"`
	Last4        string  `json:"last4"`
	ExpiryMonth  int     `json:"expiry_month"`
	ExpiryYear   int     `json:"expiry_year"`
	CVV          string  `json:"-"`                    // sempre excluído
	BillingName  string  `json:"billing_name,omitempty"` // excluído se vazio
	internalRef  string                                 // não exportado — excluído automaticamente
}

pm := PaymentMethod{
	ID: "pm_9f3a", Last4: "4242",
	ExpiryMonth: 12, ExpiryYear: 2028,
	CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", "  ")
// CVV, BillingName (vazio) e internalRef não aparecem na saída

Como trato time.Time na serialização JSON?

encoding/json serializa time.Time no formato RFC3339Nano por padrão (ex.: "2026-03-10T14:22:00Z"), compatível com ISO 8601. Se precisar de um formato diferente — como inteiros Unix epoch para uma API legada, ou uma string apenas com a data — implemente MarshalJSON() em um tipo wrapper que embuta time.Time e retorne o formato necessário.

Go
import (
	"encoding/json"
	"fmt"
	"time"
)

// Comportamento padrão — RFC3339Nano, sem código personalizado
type AuditEvent struct {
	Action    string    `json:"action"`
	OccurredAt time.Time `json:"occurred_at"`
}

e := AuditEvent{
	Action:    "fatura.paga",
	OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", "  ")
// {
//   "action": "fatura.paga",
//   "occurred_at": "2026-03-10T14:22:00Z"
// }

// Personalizado: timestamp Unix como inteiro
type UnixTime struct{ time.Time }

func (u UnixTime) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("%d", u.Unix())), nil
}

Ferramentas relacionadas

Também disponível em:PythonJavaScriptBash
JO
James OkaforSystems Engineer

James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.

TM
Tobias MüllerRevisor técnico

Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.