← Voltar para o blog

Delve em Go: Debug com Breakpoints, VS Code e Docker

Aprenda Delve em Go: instalação, breakpoints, goroutines, testes, attach, VS Code, Docker, debug remoto seguro e diagnóstico sem alterar o código-fonte.

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.

FerramentaPergunta principal
DelveQual é o estado deste processo neste ponto da execução?
Race detectorDuas goroutines acessaram memória sem sincronização adequada?
pprofOnde o programa consome CPU, memória ou espera por locks?
go tool traceComo scheduler, GC, syscalls e goroutines interagem no tempo?
Logs e tracingO que aconteceu com requisições reais ao longo do sistema?
StaticcheckExiste 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:

ComandoAção
break main.totalCria breakpoint em uma função
break main.go:14Cria breakpoint em arquivo e linha
continueContinua até o próximo breakpoint ou término
nextAvança para a próxima linha sem entrar na função chamada
stepEntra na função chamada
stepoutExecuta até sair da função atual
localsLista variáveis locais
argsLista argumentos da função
print exprAvalia uma expressão
stackMostra a call stack
listMostra o trecho de código atual
restartReinicia o processo sob debug
exitEncerra 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 é:

  1. reduza o defeito a um teste;
  2. confirme que ele falha sem Delve;
  3. abra o teste com dlv test;
  4. inspecione a primeira divergência, não apenas o erro final;
  5. corrija o código;
  6. preserve o teste como regressão;
  7. 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 WaitGroup cuja 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

ComandoQuando usar
dlv debug ./cmd/apiCompilar um pacote e iniciar sob debug
dlv test ./internal/serviceDepurar testes de um pacote
dlv exec ./bin/api -- --config dev.yamlDepurar um binário já compilado
dlv attach PIDAnexar a um processo local em execução
dlv dap --listen=127.0.0.1:38697Iniciar 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.0 no 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:

  1. reproduzir com teste;
  2. reproduzir em staging com dados anonimizados;
  3. melhorar logs, métricas, tracing e profiling;
  4. usar dump ou flight recorder quando apropriado;
  5. 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:

  1. escreva o sintoma e a entrada que reproduz;
  2. reduza para o menor teste possível;
  3. coloque o primeiro breakpoint antes da divergência;
  4. compare valor esperado e real;
  5. navegue pela stack para descobrir de onde veio o estado;
  6. corrija uma causa por vez;
  7. mantenha um teste de regressão;
  8. rode análise estática e race detector quando aplicável;
  9. remova prints, portas, capabilities e flags de debug temporárias;
  10. 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.