Debugging Go programs using Delve

Nirdosh Gautam
3 min readMar 12, 2024

Delve is a very powerful tool to debug Go programs. For example, we can:

  • Set breakpoints and execute line-by-line.
  • Trace execution flow e.g.main -> fn1 -> fn2
  • View function parameters, and local/package variables and modify their values.
  • List, filter, and examine Go routines, and switch to a specific Go routine or thread.
  • List and analyze stack frames (can be useful to debug recursive functions)
  • Examine memory used by a certain address.
  • Debug a remote Go program e.g. inside a container.

Installation

go install github.com/go-delve/delve/cmd/dlv@latest

Examples

  1. Run Go code in debug mode with breakpoint
dlv debug algo.go
Type 'help' for list of commands.
# adding breakpoint in line num 15
(dlv) break algo.go:15
Breakpoint 1 set at 0x1021828c0 for main.fibRecur() ./algo.go:15
# resume execution using 'continue' command
(dlv) continue
> main.fibRecur() ./algo.go:15 (hits goroutine(1):1 total:1) (PC: 0x1041a68c0)
14: func fibRecur(n int) int {
=> 15: if n <= 2 {
16: return 1

2. Print variables and function arguments

(dlv) args       # prints function arguments and their values
n = 30
~r0 = 0
(dlv) locals # prints local variables at this execution
(no locals)
continue
next

Show code around the current breakpoint:

(dlv) list
> main.fibRecur() ./algo.go:15 (hits goroutine(1):4 total:4) (PC: 0x102ab28c0)
14: func fibRecur(n int) int {
=> 15: if n <= 2 {
16: return 1
17: }
18: return fibRecur(n-1) + fibRecur(n-2)
19: }

Clear breakpoint and continue the code execution:

(dlv) clearall                  # deletes all breakpoints
Breakpoint 1 cleared at 0x1046c28c0 for main.fibRecur() ./algo.go:15
(dlv) continue # continues code execution
fibRecur res: 832040
finished in 51.890292084s
fibDynamic res: 832040
finished in 69µs
Process 64689 has exited with status 0

3. Display Goroutines

Lists current goroutines, including the main function and Garbage Collector.

(dlv) goroutines
* Goroutine 1 - User: ./algo.go:15 main.fibRecur (0x1021828c0) (thread 5969677)
Goroutine 2 - User: /Users/nirdosh/.gvm/gos/go1.21.1/src/runtime/proc.go:399 runtime.gopark (0x102116ba0) [force gc (idle)]
Goroutine 3 - User: /Users/nirdosh/.gvm/gos/go1.21.1/src/runtime/proc.go:399 runtime.gopark (0x102116ba0) [GC sweep wait]
Goroutine 4 - User: /Users/nirdosh/.gvm/gos/go1.21.1/src/runtime/proc.go:399 runtime.gopark (0x102116ba0) [GC scavenge wait]
Goroutine 5 - User: /Users/nirdosh/.gvm/gos/go1.21.1/src/runtime/proc.go:399 runtime.gopark (0x102116ba0) [finalizer wait]

Switch to a specific go routine :

# goroutine <num>
(dlv) goroutine 2
Switched from 1 to 2 (thread 5969677)

4. View Call Stack

(dlv) stack
0 0x0000000102ab28c0 in main.fibRecur # recursive function called 2nd time
at ./algo.go:15
1 0x0000000102ab28e8 in main.fibRecur # recursive function called 1st time
at ./algo.go:18
2 0x0000000102ab2ac8 in main.main
at ./algo.go:40
3 0x0000000102a46744 in runtime.main
at /Users/nirdosh/.gvm/gos/go1.21.1/src/runtime/proc.go:267
4 0x0000000102a71094 in runtime.goexit
at /Users/nirdosh/.gvm/gos/go1.21.1/src/runtime/asm_arm64.s:1197

See arguments in a specific call stack:

(dlv) stack 0 -full
0 0x0000000102ab28c0 in main.fibRecur
at ./algo.go:15
n = 27 # variable passed to the function 'fibRecur'
~r0 = 0

5. Tracing

(dlv) trace main.main   # enabling tracing for 'main' function
Tracepoint 1 set at 0x104afaaa0 for main.main() ./algo.go:38
(dlv) continue
> goroutine(1): main.main()
fibRecur res: 5 # fibRecur fn was called and it returned 5
finished in 41.042µs
fibDynamic res: 5. # fibDynamic fn was called and it returned 5
finished in 2.708µs
>> goroutine(1): => ()
Process 66401 has exited with status 0

5. Attach dlv into a running process

Note that the code execution can be at any point(line number) when you attach the debugger. So, factor this while adding breakpoints.

# ------- build and run go binary ----------
go build -o testbinary main.go
./testbinary

# ------- attach debugger -----------
# from another terminal session, get process id
pgrep testbinary
88848

# use pid from above
dlv attach 88848
(dlv) break main.go:28
(dlv) continue

6. Display command details

# command: help <cmd>
(dlv) help goroutines

7. Record command output from the current debug session in a file

(dlv) transcript algo_debugug.txt

8. Integration with code editors

In VSCode, dlv is bundled with the vscode-go extension.

--

--

Nirdosh Gautam

Software Engineer | Go, Ruby, and AWS | My world revolves around Tech, Music, and Bikes.