---
title: "Delve em Go: Debug com Breakpoints, VS Code e Docker"
url: "https://golang.com.br/blog/delve-debug-go-vscode-docker/"
markdown_url: "https://golang.com.br/blog/delve-debug-go-vscode-docker.MD"
description: "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."
date: "2026-07-25"
author: "Golang Brasil"
---

# 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](/blog/pprof-go-producao/) quando precisa descobrir onde CPU ou memória são consumidas; [slog](/blog/slog-go-logging-estruturado/) 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:

```bash
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:

```bash
dlv version
```

Se o shell não encontrar o comando, confira:

```bash
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:

```go
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:

```bash
dlv debug .
```

No prompt do Delve, coloque um breakpoint na função:

```text
(dlv) break main.total
(dlv) continue
```

Quando o processo parar, examine argumentos e variáveis:

```text
(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:

```go
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:

```text
(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:

```text
(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:

```text
(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:

```text
(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:

```bash
dlv test ./internal/order
```

Para executar somente um teste, passe flags do binário de teste após `--`:

```bash
dlv test ./internal/order -- -test.run '^TestService_CalculateDiscount$'
```

Dentro da sessão:

```text
(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`:

```bash
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](/aprenda/testes-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](/blog/testes-tabela-go-guia-table-driven-tests/).

## Debug de goroutines e deadlocks

Quando a aplicação parece congelada, liste as goroutines:

```text
(dlv) goroutines
```

A saída mostra IDs, localização e estado. Se uma goroutine específica parece suspeita, selecione-a:

```text
(dlv) goroutine 37
(dlv) stack
(dlv) locals
```

Também é possível executar um comando no contexto de uma goroutine e frame:

```text
(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](/erros/race-condition-data-race/). 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](/erros/deadlock-goroutines/).

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:

```bash
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`:

```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:

```json
{
  "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:

```json
"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:

```dockerfile
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:

```yaml
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:

```json
{
  "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:

```bash
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:

```bash
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](/blog/context-timeout-cancelamento-go/) e o de [graceful shutdown](/blog/graceful-shutdown-go-producao/) 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:

```bash
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:

```bash
go test ./...
go test -race ./...
go vet ./...
staticcheck ./...
```

O guia de [Staticcheck e golangci-lint](/blog/staticcheck-golangci-lint-go-qualidade-ci/) 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](/blog/httptrace-go-debug-http-client/).

Para comparar ferramentas de performance em outra linguagem de sistemas, o <a href="https://rustlang.com.br/blog/rust-profiling-performance-producao-2026/?utm_source=golang.com.br&utm_medium=referral&utm_campaign=portfolio_crosslink&utm_content=delve-debug-go-vscode-docker" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">guia de profiling em Rust do Rust Brasil</a> 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.
