Debugging Go programs using Delve

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
    
  3. 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: }
    
  4. 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
    
  5. 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]
    
  6. Switch to a specific goroutine

     # goroutine 
     (dlv) goroutine 2
     Switched from 1 to 2 (thread 5969677)
    
  7. 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
    
  8. 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
    
  9. 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
    
  10. Attach dlv to 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
    
  11. Display command details

    # command: help 
    (dlv) help goroutines
    
  12. Record command output from the current debug session in a file

    (dlv) transcript algo_debugug.txt
    
  13. Integration with code editors
    In VSCode, dlv is bundled with the vscode-go extension.

Did you find this article valuable?

Support Nirdosh Gautam by becoming a sponsor. Any amount is appreciated!