Para investigar um programa Go que entra em um estado inesperado, Delve é o debugger mais completo do ecossistema. Ele permite colocar breakpoints, avançar linha por linha, inspecionar variáveis e interfaces, navegar pela call stack e trocar de goroutine sem espalhar fmt.Println pelo código. Também funciona com testes, VS Code, processos já iniciados e containers de desenvolvimento.
A recomendação direta é usar cada ferramenta para a pergunta certa: Delve quando você precisa entender estado e fluxo de uma execução específica; pprof quando precisa descobrir onde CPU ou memória são consumidas; slog e tracing quando precisa observar várias requisições em ambientes distribuídos. O debugger é uma lupa interativa, não um substituto para observabilidade.
Neste guia, você vai instalar o Delve, depurar um programa e um teste, usar breakpoints condicionais, analisar goroutines, configurar o VS Code e executar debug dentro de Docker sem transformar a porta administrativa em uma vulnerabilidade.
O que o Delve resolve
O compilador e as ferramentas nativas já encontram muita coisa. go test, o race detector, go vet, Staticcheck e govulncheck cobrem classes diferentes de defeitos. Mesmo assim, existem situações em que você precisa parar o programa no momento exato em que o estado se torna incorreto:
- uma condição rara leva ao caminho errado;
- uma interface contém um tipo concreto inesperado;
- uma goroutine está presa esperando channel ou mutex;
- um teste falha apenas com determinada entrada;
- um parser altera um campo no meio do fluxo;
- uma aplicação funciona localmente, mas não dentro do container;
- o erro ocorre antes de uma mensagem de log útil.
Delve controla a execução do processo e consulta informações produzidas pelo compilador. Na prática, ele oferece comandos como break, continue, next, step, stepout, locals, args, print, stack, goroutines e goroutine.
| Ferramenta | Pergunta principal |
|---|---|
| Delve | Qual é o estado deste processo neste ponto da execução? |
| Race detector | Duas goroutines acessaram memória sem sincronização adequada? |
| pprof | Onde o programa consome CPU, memória ou espera por locks? |
go tool trace | Como scheduler, GC, syscalls e goroutines interagem no tempo? |
| Logs e tracing | O que aconteceu com requisições reais ao longo do sistema? |
| Staticcheck | Existe um padrão suspeito detectável sem executar o programa? |
Comece pelo método menos invasivo que responde à pergunta. Um teste de unidade pode reproduzir o bug sem debugger; uma corrida exige go test -race; um aumento gradual de heap pede perfil. Delve ganha quando o problema é fluxo mais estado.
Como instalar o Delve
Instale a CLI com o toolchain Go:
go install github.com/go-delve/delve/cmd/dlv@latest
O binário será colocado no diretório configurado em GOBIN ou em $(go env GOPATH)/bin. Confirme:
dlv version
Se o shell não encontrar o comando, confira:
go env GOBIN
go env GOPATH
Em uma máquina pessoal, @latest é conveniente para começar. Em uma equipe, documente uma versão validada ou mantenha a ferramenta em um script de bootstrap. Debuggers conversam de perto com compilador, formato do executável e sistema operacional; reproduzir a versão reduz diferenças entre notebooks.
Primeiro debug com dlv debug
Considere este programa com um erro de lógica:
package main
import "fmt"
type Order struct {
ID string
Subtotal int
Discount int
}
func total(order Order) int {
if order.Subtotal >= 10_000 {
order.Discount = 20
}
return order.Subtotal - order.Subtotal*order.Discount
}
func main() {
order := Order{ID: "pedido-42", Subtotal: 15_000}
fmt.Println(total(order))
}
O código pretendia aplicar 20%, mas armazenou 20 em vez de uma fração ou valor calculado. Inicie o pacote atual:
dlv debug .
No prompt do Delve, coloque um breakpoint na função:
(dlv) break main.total
(dlv) continue
Quando o processo parar, examine argumentos e variáveis:
(dlv) args
(dlv) locals
(dlv) print order
(dlv) next
(dlv) print order.Discount
(dlv) next
Os comandos principais são:
| Comando | Ação |
|---|---|
break main.total | Cria breakpoint em uma função |
break main.go:14 | Cria breakpoint em arquivo e linha |
continue | Continua até o próximo breakpoint ou término |
next | Avança para a próxima linha sem entrar na função chamada |
step | Entra na função chamada |
stepout | Executa até sair da função atual |
locals | Lista variáveis locais |
args | Lista argumentos da função |
print expr | Avalia uma expressão |
stack | Mostra a call stack |
list | Mostra o trecho de código atual |
restart | Reinicia o processo sob debug |
exit | Encerra a sessão |
Depois de confirmar o estado, a correção pode representar o desconto em pontos percentuais:
func total(order Order) int {
if order.Subtotal >= 10_000 {
order.Discount = 20
}
discountValue := order.Subtotal * order.Discount / 100
return order.Subtotal - discountValue
}
O debugger ajuda a localizar o desvio, mas a correção deve virar teste. Sem isso, o mesmo bug pode voltar em uma refatoração.
Breakpoints condicionais e hit count
Parar em toda iteração é impraticável quando o erro aparece apenas no item 8.421. Use uma condição:
(dlv) break processor.go:87
(dlv) condition 1 item.ID == "pedido-8421"
(dlv) continue
O primeiro argumento de condition é o ID ou nome do breakpoint. Você também pode criar a condição no mesmo comando:
(dlv) break processor.go:87 if item.ID == "pedido-8421"
Para loops em que o estado quebra depois de muitas passagens, condições por contagem ajudam:
(dlv) condition -hitcount 1 == 500
Existe ainda a contagem por goroutine com -per-g-hitcount. Use com cuidado: uma condição complexa avaliada milhares de vezes deixa a sessão lenta. Prefira filtrar pelo campo mais seletivo possível.
Um tracepoint é útil quando você quer registrar que uma linha foi alcançada sem permanecer parado manualmente em cada ocorrência:
(dlv) trace worker.process
Para investigação duradoura, porém, transforme o sinal relevante em log estruturado, métrica ou teste. O debugger deve ajudar a descobrir; não deve ser o único mecanismo para operar o sistema.
Depurando testes com dlv test
Bugs ficam mais fáceis de investigar quando cabem em um teste determinístico. Use dlv test para compilar o binário de testes com as opções adequadas e iniciar o debugger:
dlv test ./internal/order
Para executar somente um teste, passe flags do binário de teste após --:
dlv test ./internal/order -- -test.run '^TestService_CalculateDiscount$'
Dentro da sessão:
(dlv) break example.com/shop/internal/order.(*Service).Calculate
(dlv) continue
(dlv) args
(dlv) locals
Para um subteste, a expressão pode incluir o nome completo reconhecido por go test:
dlv test ./internal/order -- -test.run 'TestService_CalculateDiscount/cliente_premium'
O fluxo recomendado é:
- reduza o defeito a um teste;
- confirme que ele falha sem Delve;
- abra o teste com
dlv test; - inspecione a primeira divergência, não apenas o erro final;
- corrija o código;
- preserve o teste como regressão;
- rode
go test ./...e, quando houver concorrência,go test -race ./....
A página de testes em Go cobre t.Run, t.Cleanup, t.TempDir, httptest, erros e benchmarks. Para muitos cenários de entrada, combine o debugger com table-driven tests.
Debug de goroutines e deadlocks
Quando a aplicação parece congelada, liste as goroutines:
(dlv) goroutines
A saída mostra IDs, localização e estado. Se uma goroutine específica parece suspeita, selecione-a:
(dlv) goroutine 37
(dlv) stack
(dlv) locals
Também é possível executar um comando no contexto de uma goroutine e frame:
(dlv) goroutine 37 frame 2 locals
Procure padrões como:
- várias goroutines esperando envio para um channel sem consumidor;
- recebimento de channel que nunca será fechado;
- chamadas de rede sem timeout;
- mutex mantido durante I/O;
- worker esperando
WaitGroupcuja contagem não chega a zero; - goroutine criada por request que ignora cancelamento do contexto.
Delve mostra um retrato da execução. Para uma corrida de dados, execute o race detector. Para crescimento contínuo da quantidade de goroutines em produção, colete o goroutine profile com pprof. Para um travamento conhecido, consulte também o guia de deadlock em Go.
Evite alterar variáveis aleatoriamente só para fazer o processo continuar. O comando set existe, mas modificar estado pode esconder a causa e produzir um caminho que nunca ocorreria normalmente. Primeiro capture stack, valores e sequência de eventos.
dlv debug, exec, test e attach: qual usar
| Comando | Quando usar |
|---|---|
dlv debug ./cmd/api | Compilar um pacote e iniciar sob debug |
dlv test ./internal/service | Depurar testes de um pacote |
dlv exec ./bin/api -- --config dev.yaml | Depurar um binário já compilado |
dlv attach PID | Anexar a um processo local em execução |
dlv dap --listen=127.0.0.1:38697 | Iniciar servidor para um cliente compatível com DAP |
dlv attach depende de permissões do sistema operacional. Em Linux, políticas de ptrace, usuário do processo e capabilities podem bloquear o attach. Em ambientes corporativos, não desative proteções globalmente sem entender o impacto; prefira iniciar uma instância de desenvolvimento diretamente com Delve.
Para encontrar o PID:
pgrep -af minha-api
dlv attach 12345
Ao encerrar uma sessão de attach, confirme se o processo deve continuar ou ser terminado. Em uma aplicação compartilhada, pausar todas as goroutines pode interromper requests, leases e heartbeats. Isso é um dos motivos para não tratar attach em produção como rotina.
Configurando o VS Code com dlv-dap
A extensão oficial Go para VS Code usa o adaptador DAP do Delve. Para um pacote principal, crie .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug API",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/api",
"args": ["--config", "config/dev.yaml"],
"env": {
"APP_ENV": "development"
}
}
]
}
Selecione a configuração e pressione F5. O editor permite criar breakpoints na margem, inspecionar variáveis, avaliar expressões, navegar pela stack e escolher goroutines.
Para um pacote de testes:
{
"name": "Debug testes do pacote atual",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${fileDirname}",
"args": ["-test.run", "^TestService_CalculateDiscount$"]
}
Em projetos que usam symlinks, workspace remoto ou caminhos diferentes dentro de container, substitutePath traduz caminho local e remoto:
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/workspace"
}
]
Não copie um launch.json enorme antes de testar o default. Comece com program, mode e argumentos. Adicione substitutePath, attach remoto e opções de carregamento de variáveis apenas quando houver um problema concreto.
Debug de uma aplicação Go no Docker
Para um container de desenvolvimento, crie uma imagem com Delve e compile sem otimizações que dificultem o mapeamento de linhas e variáveis:
FROM golang:1.26 AS debug
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go install github.com/go-delve/delve/cmd/dlv@latest
RUN go build -gcflags="all=-N -l" -o /workspace/bin/api ./cmd/api
EXPOSE 40000
CMD ["dlv", "exec", "/workspace/bin/api", "--headless", "--listen=0.0.0.0:40000", "--api-version=2", "--accept-multiclient", "--", "--config", "config/dev.yaml"]
As flags -N -l desabilitam otimizações e inlining para melhorar a experiência de debug. Não use esse binário como artefato de produção: ele é maior, mais lento e construído para investigação.
No Compose, restrinja a porta ao host local e conceda somente o necessário para o debugger:
services:
api-debug:
build:
context: .
target: debug
ports:
- "127.0.0.1:40000:40000"
cap_add:
- SYS_PTRACE
security_opt:
- seccomp:unconfined
SYS_PTRACE e seccomp:unconfined reduzem isolamento e devem ficar em um arquivo ou profile de desenvolvimento, nunca no serviço normal por conveniência. Dependendo do ambiente, uma configuração menos permissiva pode funcionar; comece pelo mínimo aceito pela sua plataforma.
No VS Code, conecte ao servidor:
{
"name": "Attach API no Docker",
"type": "go",
"request": "attach",
"mode": "remote",
"host": "127.0.0.1",
"port": 40000,
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/workspace"
}
]
}
Se os breakpoints aparecem como não verificados, confira três coisas: o binário foi compilado a partir do mesmo código aberto no editor; os caminhos de origem batem; e substitutePath aponta para o diretório correto dentro do container.
Debug remoto sem expor o processo
Uma porta Delve permite controlar e inspecionar o processo. Portanto:
- não publique a porta em
0.0.0.0no host; - não coloque o endpoint atrás de um domínio público;
- não reutilize uma porta aberta permanentemente em produção;
- não presuma que o protocolo oferece uma fronteira de autenticação adequada;
- não grave dumps ou variáveis sensíveis no repositório.
Quando o processo está em uma máquina remota, use túnel SSH:
ssh -L 40000:127.0.0.1:40000 usuario@servidor-interno
O Delve remoto escuta apenas em localhost no servidor, e o editor conecta a 127.0.0.1:40000 na sua máquina. Em Kubernetes, prefira um pod de staging e port-forward temporário:
kubectl -n staging port-forward pod/api-debug-abc123 40000:40000
Para produção, a ordem de preferência costuma ser:
- reproduzir com teste;
- reproduzir em staging com dados anonimizados;
- melhorar logs, métricas, tracing e profiling;
- usar dump ou flight recorder quando apropriado;
- considerar attach remoto apenas em incidente controlado, com autorização operacional.
Pausar o processo pode disparar timeout em clientes, perder liderança, atrasar renovação de lease e afetar probes. O guia de context e cancelamento e o de graceful shutdown ajudam a construir serviços que falham e encerram de maneira observável.
Problemas comuns no Delve
Breakpoint não é atingido
A função pode não estar no caminho executado, ter sido inlined ou pertencer a outro binário. Confirme com um breakpoint anterior e, quando você controla o build, use:
go build -gcflags="all=-N -l" -o bin/app ./cmd/app
dlv debug e dlv test já preparam builds para debug; a flag manual é mais relevante para dlv exec sobre um binário compilado por você.
Variável aparece como otimizada
O binário provavelmente foi construído com otimizações normais. Recompile para debug. Ainda assim, algumas variáveis podem não existir no ponto escolhido por causa de escopo e transformação do compilador.
Attach retorna operação não permitida
Verifique usuário, política ptrace, container capability e seccomp. Não resolva com container privilegiado ou mudança global de kernel como primeira opção. Uma imagem de debug iniciada pelo próprio Delve costuma ser mais previsível.
Source path não encontrado
O executável foi compilado em caminho diferente do workspace atual. Configure substitutePath ou reproduza o mesmo diretório no container.
A inspeção de variável está lenta
Slices, mapas, strings e structs enormes custam para carregar. No VS Code, os limites de valores e tamanho de strings evitam transferências excessivas. Inspecione o campo necessário em vez de expandir uma árvore gigantesca.
O bug desaparece sob o debugger
O debugger altera timing. Problemas concorrentes podem sumir quando o processo é pausado, um caso clássico de heisenbug. Use go test -race, testes repetidos com -count, métricas e trace. Não conclua que o defeito foi resolvido só porque não ocorreu durante a sessão.
Um fluxo de debug que deixa o sistema melhor
Uma sessão útil deve terminar com um artefato permanente:
- escreva o sintoma e a entrada que reproduz;
- reduza para o menor teste possível;
- coloque o primeiro breakpoint antes da divergência;
- compare valor esperado e real;
- navegue pela stack para descobrir de onde veio o estado;
- corrija uma causa por vez;
- mantenha um teste de regressão;
- rode análise estática e race detector quando aplicável;
- remova prints, portas, capabilities e flags de debug temporárias;
- registre uma métrica ou log se o problema precisa ser detectável em produção.
Depois da correção, execute uma esteira mínima:
go test ./...
go test -race ./...
go vet ./...
staticcheck ./...
O guia de Staticcheck e golangci-lint mostra como transformar essas verificações em política de CI. Se a investigação envolve uma chamada HTTP, use também httptrace para diagnosticar DNS, conexão e TLS.
Para comparar ferramentas de performance em outra linguagem de sistemas, o guia de profiling em Rust do Rust Brasil mostra como o mesmo princípio — medir antes de otimizar — aparece com ferramentas diferentes.
Checklist de debug seguro
- O problema foi reproduzido em teste ou ambiente controlado.
- A versão de Delve é conhecida pela equipe.
- O binário de debug não será publicado como release.
- A porta Delve está restrita a localhost, túnel ou port-forward.
- Capabilities de container existem apenas no profile de desenvolvimento.
- Breakpoints condicionais filtram casos de alto volume.
- Dados sensíveis não foram copiados para screenshots, dumps ou tickets públicos.
- A sessão não está pausando um serviço compartilhado sem coordenação.
- A correção ganhou teste de regressão.
- Prints, flags e acessos temporários foram removidos.
Perguntas frequentes
O que é Delve em Go?
Delve é um debugger específico para Go. Ele controla a execução de programas e testes, cria breakpoints, inspeciona variáveis, mostra a call stack e permite navegar entre goroutines. É a ferramenta usada por integrações de debug em editores como VS Code.
Como instalar o Delve?
Execute go install github.com/go-delve/delve/cmd/dlv@latest e confirme com dlv version. Se o comando não estiver no PATH, adicione $(go env GOPATH)/bin ou o diretório configurado em GOBIN.
Qual é a diferença entre dlv debug, dlv test e dlv attach?
dlv debug compila e inicia um pacote; dlv test compila o binário de testes; dlv attach conecta a um processo existente. Para investigar um executável já compilado, use dlv exec.
Delve funciona com VS Code?
Sim. A extensão Go usa dlv-dap, a integração DAP do Delve. Uma configuração launch inicia o programa ou teste pelo editor. Uma configuração attach conecta a um processo ou servidor remoto existente.
É seguro expor o Delve em produção?
Não exponha o Delve publicamente. Ele pode pausar o programa e inspecionar estado interno. Se um debug remoto for indispensável, restrinja a localhost ou rede administrativa, conecte por túnel e limite a duração. Na maioria dos casos, pprof, tracing, logs e uma reprodução em staging são alternativas mais seguras.
Próximo passo
Escolha um teste pequeno do seu projeto e abra-o com dlv test. Coloque um breakpoint antes da asserção, use locals, print, stack, next e step, depois transforme o que descobriu em um teste mais preciso. Em seguida, configure o mesmo fluxo no VS Code sem adicionar Docker ou attach remoto antes de precisar deles.
Uma boa prática de debug não é saber decorar todos os comandos. É reduzir a incerteza sem degradar o ambiente: reproduzir, observar o primeiro desvio, corrigir a causa e deixar teste e telemetria melhores do que estavam antes.