---
title: "Fuzz Testing em Go: Encontrando Bugs com testing.F"
url: "https://golang.com.br/blog/fuzzing-go-testes-nativos/"
markdown_url: "https://golang.com.br/blog/fuzzing-go-testes-nativos.MD"
description: "Aprenda fuzz testing em Go com testing.F: crie fuzz targets, gere corpus, descubra panics e bugs de parsing/segurança que testes de tabela não encontram. Go nativo, sem dependências."
date: "2026-06-21"
author: "Golang Brasil"
---

# Fuzz Testing em Go: Encontrando Bugs com testing.F

Aprenda fuzz testing em Go com testing.F: crie fuzz targets, gere corpus, descubra panics e bugs de parsing/segurança que testes de tabela não encontram. Go nativo, sem dependências.


Testes de tabela respondem a uma pergunta: "o que eu imaginei que pudesse quebrar, quebra?". Fuzz testing responde a uma pergunta bem mais incômoda: "o que eu **n** imaginei que pudesse quebrar, quebra?". É essa a diferença que torna o fuzz nativo do Go (estável desde o 1.18) uma das ferramentas de qualidade mais subutilizadas da linguagem: em vez de você alimentar o teste com entradas pré-listadas, o runtime gera milhares de entradas aleatórias e mutacionais e tenta derrubar seu código.

Neste guia você vai escrever fuzz targets idiomáticos, gerar e versionar um corpus, reproduzir e corrigir um bug encontrado pelo fuzzer e integrar tudo ao CI — sem nenhuma biblioteca externa, usando apenas `go test -fuzz`. Ele complementa o [guia de table-driven tests](/blog/testes-tabela-go-guia-table-driven-tests/), o [guia de Testcontainers](/blog/go-testcontainers-testes-integracao-containers/), o [guia de erros com errors.Join](/blog/errors-join-go-multiplos-erros-producao/) e o [guia de pprof em produção](/blog/pprof-go-producao/), cobrindo a faixa de qualidade entre "cobre os caminhos felizes" e "encontra inputs patológicos que ninguém previu".

## Por que fuzz testing importa

Testes unitários clássicos são necessários, mas têm uma falácia embutida: só testam o que o autor do teste considerou. Para um parser de JSON, um validador de CPF, um decodificador de query string ou um serializador de protobuf, a superfície de entrada é praticamente infinita. O autor escreve dez casos "típicos" e talvez dois "de borda", mas bugs reais — integer overflow, UTF-8 malformado, strings gigantes, caracteres nulos no meio do input, recursão exagerada — vivem exatamente fora desses dez casos.

Fuzz testing inverte o controle. Em vez do autor escolher a entrada, o motor de fuzz gera entradas usando mutação guiada por cobertura: parte de seeds conhecidos, mede quais caminhos do código cada input exercita e muta os inputs que chegam a código novo. O objetivo não é "aleatoriedade pura", é **cobertura crescente**. Quando um input causa panic, `nil dereference`, index out of range, timeout ou qualquer asserção falha, o fuzzer grava o input ofensivo em `testdata/fuzz/<NomeDoTeste>` e o teste falha de forma reproduzível.

Os bugs que fuzz encontra costumam ser exatamente os que viram CVEs em parsers e protocolos: uma string de 2GB que esgota memória, um campo negativo que vira índice inválido, um byte `0x00` que confunde parsers C-style. Em Go, onde panics em produção matam a goroutine (e às vezes o processo inteiro se não houver `recover`), encontrar essas entradas antes do deploy é barato e valioso.

## Quando usar (e quando não usar)

Fuzz brilha em **funções que recebem input não confiável e produzem output ou estado**. Os alvos típicos são:

- Parsers: JSON, YAML, TOML, query string, headers HTTP, mensagens de protocolo binário.
- Validadores: CPF/CNPJ, e-mail, URL, telefone, schema.
- Serializadores/deserializadores: protobuf, MessagePack, CSV, formatos próprios.
- Funções cripto-adjacentes: normalização de string, comparação de hashes, decodificação Base64/Bearer token.
- Filtros e transformações: sanitização de HTML, escape de SQL, normalização de path.

Fuzz **não** substitui testes de unidade. Ele é complementar:

- Teste de tabela: afirma comportamento esperado para entradas conhecidas.
- Fuzz: afirma **invariante** para entradas arbitrárias (ex.: "nunca panic", "se parseou, o output re-serializa igual", "tempo limitado").

Se sua funções não recebe input externo, ou se a invariante não é fácil de expressar, fuzz agrega pouco. Não fuzze um cálculo puro sem ramificação: não há o que descobrir.

## Anatomia de um fuzz target

Um fuzz target é uma função `FuzzXxx(*testing.F)`, irmã da `TestXxx(*testing.T)`. Dentro dela você chama `f.Add(...)` para registrar seeds e `f.Fuzz(func(t *testing.T, ...))` para declarar a invariante. O motor cuida do resto.

```go
package parser

import (
	"errors"
	"strconv"
	"testing"
)

var ErrNotPositive = errors.New("must be positive")

// ParsePositiveInt converte string em int e exige valor > 0.
func ParsePositiveInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	if n <= 0 {
		return 0, ErrNotPositive
	}
	return n, nil
}

func FuzzParsePositiveInt(f *testing.F) {
	// Seeds: o ponto de partida do motor de mutação.
	f.Add("1")
	f.Add("42")
	f.Add("999999")
	f.Add("-5")
	f.Add("0")
	f.Add("")

	f.Fuzz(func(t *testing.T, s string) {
		n, err := ParsePositiveInt(s)

		// Invariante 1: nunca panic.
		// (implícita: se a função panicar, o teste falha sozinho)

		// Invariante 2: contrato consistente.
		if err == nil && n <= 0 {
			t.Errorf("aceitou valor não positivo: %q -> %d", s, n)
		}
		if err != nil && n != 0 {
			t.Errorf("retornou erro e valor não-zero: %q -> %d", s, n)
		}

		// Invariante 3: round-trip (se parseou, re-serializa igual).
		if err == nil {
			if strconv.Itoa(n) != s {
				// Pode ser legítimo para "007" -> 7; aqui flaggamos para revisar.
				t.Logf("round-trip divergiu: %q -> %d", s, n)
			}
		}
	})
}
```

Três pontos-chave:

1. **`f.Add` registra seeds.** O motor muta a partir deles. Seeds bons cobrem caminhos felizes e bordas óbvias (vazio, negativo, zero, muito grande).
2. **A função dentro de `f.Fuzz` é o coração do target.** É nela que você expressa a invariante. Não tente adivinhar qual input virá — **qualquer** input pode chegar.
3. **Asserções são sobre invariantes, não valores exatos.** Você não sabe o input, então não pode prever o output. Mas pode prever propriedades: nunca panic, nunca retorna estado inválido, sempre re-serializa igual.

O tipo de cada argumento de `f.Fuzz` e `f.Add` deve ser um dos **tipos suportados**: `string`, `[]byte`, `int`, `int8/16/32/64`, `uint*`, `float32/64`, `bool`, `rune`. Para structs complexas, passe `string` ou `[]byte` e desserialize dentro do target — é assim que você fuzz de parsers de protocolo.

## Rodando o fuzzer

O fluxo tem dois modos: `go test` normal (consome corpus existente, não gera nada novo) e `go test -fuzz=...` (gera inputs indefinidamente até achar um bug ou você cancelar).

### Modo corpus (rápido, entra no CI)

```sh
go test ./...
go test -run=^FuzzParsePositiveInt$ ./...
```

Sem `-fuzz`, o Go roda os seeds e qualquer entrada que já exista em `testdata/fuzz/FuzzParsePositiveInt/`. É rápido, determinístico e deveria ser parte do `go test ./...` normal. Se um bug for encontrado e salvo no corpus, ele falha para sempre até você corrigir — exatamente como um teste de regressão.

### Modo geração (longo, rodado localmente ou em job dedicado)

```sh
go test -fuzz=^FuzzParsePositiveInt$ -fuzztime=2m ./...
```

Com `-fuzz`, o motor gera inputs novos durante o tempo definido. Quando encontra um panic ou falha de asserção, ele:

1. Para o processo com saída não-zero.
2. Escreve o input ofensivo em `testdata/fuzz/FuzzParsePositiveInt/<hash>`.
3. Imprime o caminho do arquivo e a stack trace.

A partir desse momento, o bug vira um caso de regressão: rodar `go test` simples já o reproduz.

### Limites práticos

- `-fuzz` aceita **um único** target por invocação (use `-fuzz=Parse` para filtrar por nome).
- A geração é CPU-bound e paralela por padrão (`-parallel`). Em CI dedique um job separado, não misture com o `go test ./...` rápido.
- Defina `-fuzztime` sempre. Sem limite, o processo roda até você matar (útil em job noturno, perigoso em laptop).
- Use `-fuzzminimizetime` para controlar quanto tempo o motor gasta tentando reduzir o input ofensivo ao menor caso reproduzível — útil quando o bug apareceu num input de 8KB e você quer um de 12 bytes.

## Um bug real encontrado por fuzz

Vamos rodar o target acima por 30 segundos e ver o que acontece. Em uma implementação ingênua, é comum esquecer que `strconv.Atoi` aceita underscores (`"1_000"`), sinais (`"+5"`) e whitespace ao redor (`"  7 "`):

```sh
$ go test -fuzz=^FuzzParsePositiveInt$ -fuzztime=30s ./...
--- FAIL: FuzzParsePositiveInt
    parse_test.go:42: aceitou valor não positivo: "+0" -> 0
    parse_test.go:47: round-trip divergiu: "007" -> 7
--- FAIL: FuzzParsePositiveInt
    parse_test.go:42: aceitou valor não positivo: " 10 " -> 10
```

O motor achou que `"+0"` e `" 10 "` passaram pela validação de `> 0` em caminhos que o autor do teste original não imaginou. O Go salvou os inputs:

```sh
$ cat testdata/fuzz/FuzzParsePositiveInt/a1b2c3
go test fuzz v1
[]byte("0")
$ cat testdata/fuzz/FuzzParsePositiveInt/d4e5f6
go test fuzz v1
[]byte("+0")
```

A correção pode ser normalizar (`strings.TrimSpace`), rejeitar explicitamente (`strings.ContainsAny(s, "+_ ")`) ou mudar de `strconv.Atoi` para um parser estrito. O ponto é: **o bug agora é visível e reproduzível**, e o corpus garante que ele nunca mais volta silenciosamente.

## Versionando o corpus

O diretório `testdata/fuzz/` deve entrar no git. Cada entrada é um caso de regressão permanente. Boas práticas:

- Commite cada novo crash assim que confirmar a correção — ele vira teste de regressão.
- Se o corpus crescer demais (milhares de arquivos), separe em um job de fuzz "exploratório" que roda fora do CI rápido e só promove crashes reais para `testdata/fuzz/`.
- Não commite lixo: arquivos grandes (>10KB) ou duplicados poluem o repo. O `-fuzzminimizetime` ajuda a reduzir o tamanho.

## Invariantes úteis por categoria

Expressar a invariante certa é a arte do fuzz. Aqui vão padrões reutilizáveis:

### Parsers (round-trip)

```go
f.Fuzz(func(t *testing.T, raw []byte) {
	parsed, err := MyParser(raw)
	if err != nil {
		return // erro é caminho válido
	}
	// Invariante: re-serializar o parsed produz bytes equivalentes.
	out := MySerializer(parsed)
	parsed2, err := MyParser(out)
	if err != nil {
		t.Fatalf("re-parse falhou após round-trip: %v", err)
	}
	if !Equal(parsed, parsed2) {
		t.Errorf("round-trip não estável: %+v != %+v", parsed, parsed2)
	}
})
```

### Validadores (idempotência e simetria)

```go
f.Fuzz(func(t *testing.T, s string) {
	ok1 := IsValid(s)
	normalized := Normalize(s)
	ok2 := IsValid(normalized)
	// Invariante: normalizar algo válido produz algo válido.
	if ok1 && !ok2 {
		t.Errorf("Normalize quebrou validade: %q -> %q", s, normalized)
	}
})
```

### Funções que prometem não-panic

```go
f.Fuzz(func(t *testing.T, s string) {
	// A simples execução sem panic já é a invariante.
	// Se você quiser checar mais, adicione asserções.
	_ = SafeFunc(s)
})
```

### Serialização com tamanho limitado (anti-DoS)

```go
f.Fuzz(func(t *testing.T, s string) {
	start := time.Now()
	_ = HeavyFunc(s)
	// Invariante: nunca demora mais que 100ms, qualquer input.
	if time.Since(start) > 100*time.Millisecond {
		t.Errorf("HeavyFunc lento para input de %d bytes", len(s))
	}
})
```

Esse último padrão é poderoso para detectar ReDoS (regular expression denial of service) e algoritmos quadráticos que viram DoS em produção. Combine com o [guia de pprof](/blog/pprof-go-producao/) para confirmar o gargalo.

## Fuzz no CI

A integração com CI tem duas camadas:

1. **Por commit/PR**: rode `go test ./...` normalmente. Isso consome o corpus versionado e falha se um crash histórico regrediu. É barato.
2. **Job periódico dedicado**: rode `go test -fuzz=... -fuzztime=10m` para cada target crítico, de hora em hora ou uma vez por dia. Descobrir bugs de fuzz não deve bloquear PR; deve abrir issue.

Exemplo de job dedicado em GitHub Actions:

```yaml
name: fuzz
on:
  schedule:
    - cron: "0 3 * * *"
jobs:
  fuzz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
      - run: go test -fuzz=^FuzzParse$ -fuzztime=10m ./internal/parser/
      - run: go test -fuzz=^FuzzValidate$ -fuzztime=10m ./internal/validator/
```

Se o job achar um crash, ele falha, abre issue e o input fica no log. Em seguida um humano reproduz localmente, adiciona ao corpus e corrige.

## Armadilhas comuns

### 1. Asserções fracas

```go
// Ruim: não afirma nada. O motor vai rodar milhões de inputs sem aprender nada.
f.Fuzz(func(t *testing.T, s string) {
	_ = Parse(s)
})
```

Sem asserção sobre o output, o fuzzer só detecta panics. Panics são ótimos, mas a maior parte dos bugs lógicos não panicam — produzem resultados errados silenciosamente. Adicione invariantes (round-trip, simetria, contrato).

### 2. Seeds pobres

Se todos os seeds forem `"abc"`, `"123"`, `"hello"`, o motor demora muito para descobrir caminhos exóticos. Inclua bordas reais: vazio, Unicode, null bytes, tamanhos absurdos, caracteres de controle, dígitos negativos, notação científica, JSON aninhado fundido.

### 3. Side-effects fora do target

```go
// Ruim: muta estado global; dificulta reprodução determinística.
var cache = map[string]int{}

f.Fuzz(func(t *testing.T, s string) {
	cache[s]++ // cada execução interfere na próxima
})
```

Fuzz targets devem ser puros ou isolados. Use variáveis locais, evite mutar globals, resete estado em `t.Cleanup`. Caso contrário, reproduzir o crash vira impossível.

### 4. Tempo ilimitado

Sem `-fuzztime`, o processo roda para sempre. Em laptop, isso drena bateria; em CI, trava o job. Sempre defina o limite.

### 5. Misturar fuzz com benchmark

Fuzz não mede performance — mede corretude sob entradas arbitrárias. Para medir performance, use `testing.B` combinado com [pprof](/blog/pprof-go-producao/) e [PGO](/blog/go-pgo-profile-guided-optimization-performance/). Para encontrar ReDoS via tempo, o padrão "assert duração máxima" acima resolve, mas não use fuzz como benchmark sistemático.

## Avançado: fuzzing de protocolos binários

Para fuzz de gRPC/protobuf, mensagens binárias ou formatos próprios, passe `[]byte` e desserialize dentro do target:

```go
func FuzzProtobufMessage(f *testing.F) {
	// Seeds em bytes válidos.
	f.Add([]byte{0x0a, 0x05, 'h', 'e', 'l', 'l', 'o'})
	f.Add([]byte{})

	f.Fuzz(func(t *testing.T, raw []byte) {
		var msg pb.MyMessage
		if err := proto.Unmarshal(raw, &msg); err != nil {
			return // bytes inválidos são esperados
		}
		// Invariante: re-marshalar produz bytes equivalentes (não idênticos,
		// mas semanticamente iguais).
		out, err := proto.Marshal(&msg)
		if err != nil {
			t.Fatalf("marshal falhou após unmarshal: %v", err)
		}
		var msg2 pb.MyMessage
		if err := proto.Unmarshal(out, &msg2); err != nil {
			t.Fatalf("segundo unmarshal falhou: %v", err)
		}
		if !proto.Equal(&msg, &msg2) {
			t.Errorf("round-trip protobuf instável")
		}
	})
}
```

Isso pega bugs em bindings gerados, campos desconhecidos mal tratados, loops de recursão em mensagens aninhadas e problemas de tamanho — exatamente o tipo de coisa que vira CVE em [APIs gRPC](/blog/grpc-go-protobuf-apis-internas/).

## Conclusão

Fuzz testing nativo do Go é barato de adotar e desproporcionalmente valioso. Em meia hora você transforma um parser "testado" em um parser **validado contra milhares de inputs que você nunca imaginou**. Os passos para começar agora:

1. Escolha uma função que recebe input externo (parser, validador, deserializador).
2. Escreva um `FuzzXxx` com 5–10 seeds cobrindo caminhos felizes e bordas.
3. Expresse uma invariante clara (round-trip, simetria, não-panic, duração máxima).
4. Rode `go test -fuzz=^FuzzXxx$ -fuzztime=2m` localmente.
5. Commite qualquer crash encontrado como caso de regressão.
6. Adicione `go test ./...` ao CI (consome o corpus) e um job periódico com `-fuzztime` maior.

A regra prática: **se sua função recebe bytes que um usuário ou sistema externo controla, ela deveria ter um fuzz target**. Bugs de parser são caros em produção e quase gratuitos de encontrar com fuzz — desde que você escreva o target.

## Leituras relacionadas

- [Table-driven tests em Go](/blog/testes-tabela-go-guia-table-driven-tests/) — a base sobre a qual o fuzz se apoia.
- [Testcontainers em Go](/blog/go-testcontainers-testes-integracao-containers/) — quando o fuzz precisa de um serviço real para reproduzir.
- [errors.Join para múltiplos erros](/blog/errors-join-go-multiplos-erros-producao/) — propagar erros de parsing sem perder contexto.
- [pprof em produção](/blog/pprof-go-producao/) — confirmar gargalos que o fuzz apontou como lentos.
- [PGO: Profile-Guided Optimization](/blog/go-pgo-profile-guided-optimization-performance/) — otimizar os hot paths que o fuzz ajudou a mapear.
- [gRPC e Protobuf em Go](/blog/grpc-go-protobuf-apis-internas/) — alvo clássico de fuzz de protocolo binários.
- [Go para backend](/aprenda/golang-para-backend/) — contexto onde parsers e validadores vivem.
- [Testes em Go](/aprenda/testes-go/) — visão geral da suíte de testes da linguagem.
