Debug Go with Bugsaur¶
Adapter: dlv (Delve)
Install the adapter¶
Configure¶
bugsaur init detects the project by go.mod and writes one profile per
directory containing package main: the module root itself, and direct
subdirectories of cmd/.
A single binary in the root¶
The working directory is the root and .env is read — the same as go run .
from the root.
Several commands under cmd/¶
go-order/
├── go.mod
├── .env
├── configs/dev.yaml
├── migrations/
└── cmd/
├── go-order/main.go package main
├── worker/main.go package main
└── shared/shared.go package shared ← not a command, no profile
version = 1
default = "go-order"
[profiles.go-order]
adapter = "dlv"
program = "cmd/go-order"
[profiles.worker]
adapter = "dlv"
program = "cmd/worker"
Both profiles share one working directory — the module root — so .env,
configs/ and migrations/ are visible to each command and cwd is not
needed. Switch with :DebugStart worker.
Bugsaur builds Go itself¶
Unlike the other languages, Go binaries are built by Bugsaur before the adapter
starts, in a building phase, and the adapter receives a finished binary. You
do not configure this — it is on by default.
Why. dlv debug builds inside the launch request, puts the result in
__debug_bin<random> and deletes it afterwards. Go does not cache a linked
executable — it reuses one only at the same path. The random name meant every
run paid for a full link. Measured on a service with a 74 MB binary:
dlv debug |
Bugsaur's own build | |
|---|---|---|
| first run | 14 s | 25 s |
| repeat run, code unchanged | 14 s every time | ~2 s |
Where. <root>/.bugsaur/build/<program name> — a stable path, which is the
whole point. The key is the program, not the profile, so two profiles for one
binary reuse a single build. The directory ignores itself, so there is nothing to
add to your .gitignore.
With what. By default, what the adapter catalog knows about dlv:
go build -gcflags=all=-N -l -o {out} {program}. A profile can override it:
[profiles.worker]
adapter = "dlv"
program = "cmd/worker"
[profiles.worker.build]
command = "go"
args = ["build", "-tags=dev", "-gcflags=all=-N -l", "-o", "{out}", "./cmd/worker"]
command and args are separate rather than one string: -gcflags=all=-N -l
contains a space inside a single argument, and parsing a string would need
quoting rules exactly where a mistake quietly breaks debug information.
Substitutions: {out} is the binary being built, {program} is what program
names.
When it does not build: in attach mode; for adapters with no build in the
catalog; and when the profile names mode or program to the adapter itself in
launch_arguments — that means you are driving the launch by hand.
If the build fails, the session goes to failed with the compiler's text and the
adapter is never started.
Debug a test¶
[profiles.orders-test]
adapter = "dlv"
program = "internal/orders"
# `go test` runs the binary from the package directory — without this line
# `testdata/` is not found, because cwd defaults to the root.
cwd = "internal/orders"
[profiles.orders-test.launch_arguments]
mode = "test"
Or use the test-under-cursor flow, which needs no profile:
Long builds: hand Delve a finished binary¶
If the build is always slow, take it out of the session entirely:
launch then answers immediately. The cost is rebuilding by hand after each
edit.
Two different mode keys
mode at the profile level is launch / attach. mode inside
launch_arguments is Delve's own mode — debug, test, exec — and it
overrides the generated debug.
Monorepos and go.work¶
The root is the nearest module. A shared .env above it is one line:
# services/api/.bugsaur/config.toml
[profiles.api]
adapter = "dlv"
program = "."
env_file = "../../.env"
The marker is go.mod; detection does not know about go.work.
Diagnostics¶
| Symptom | Likely cause | Where to look |
|---|---|---|
| Session fails with compiler output | the build failed | fix the code; the text is in the Logs panel |
launch hangs for minutes |
Delve is building inside the request | use mode = "exec", above |
| Thousands of threads listed | those are goroutines | Threads |
testdata/ not found in a test |
cwd defaults to the root |
set cwd to the package directory |
| Session never starts | dlv not on PATH |
Adapter fails to start |