the ‘code’ for this article can be found here: https://github.com/peter-mcconnell/petermcconnell.com/blob/main/assets/dummy/godebug/main.go

debugging Golang - the context

This is the tool I reach for when a program isn’t behaving how I expect it to and I want to dig into the internals / browse the state of the program at particular points so as to realise why my understanding of the program is wrong.

what are the requirements?

The debugger of choice (for me) is dlv / delve. The reasons for this are at the end of the article.

Install delve: https://github.com/go-delve/delve

git clone https://github.com/go-delve/delve
cd delve
go install github.com/go-delve/delve/cmd/dlv

Have the following pieces of information:

  • a list of 1 or more points in my code where I’d like to set breakpoints
    • filename and line numbers

example program

For the sake of this article we’ll create a simple application to debug:

// main.go
package main

import "fmt"

func doubleit(val int) int {
        return val * 3  // should be * 2
}

func main() {
        fmt.Printf("doubleit 2: %d\n", doubleit(2))
        fmt.Printf("doubleit 4: %d\n", doubleit(4))
        fmt.Printf("doubleit 8: %d\n", doubleit(8))
}

When we run this with go run main.go we want it to double the numbers we pass but for some reason we’re getting different results.

We will also need a go.mod for Delve to run so also perform a go mod init from this directory.

debug it

To get started from the project directory

dlv debug
Type 'help' for list of commands.
(dlv)

Now set some breakpoints. For this example we’ll say we want to debug our doubleit method - the first line of which is at :7:

dlv debug
Type 'help' for list of commands.
(dlv) b main.go:7
Breakpoint 1 set at 0x49c8bb for main.doubleit() ./main.go:7

When we’ve added all of the breakpoints that we need we can instruct the program to run with c:

dlv debug
Type 'help' for list of commands.
(dlv) b main.go:7
Breakpoint 1 set at 0x49c8bb for main.doubleit() ./main.go:7
(dlv) c
> main.doubleit() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x49c8bb)
     2:	package main
     3:
     4:	import "fmt"
     5:
     6:	func doubleit(val int) int {
=>   7:		return val * 3 // should be * 2
     8:	}
     9:
    10:	func main() {
    11:		fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12:		fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv)

Now we’ve ran our program with an attached debugger and it has paused execution at the breakpoint we set. We can run args to see which arguments where passed to the method:

(dlv) c
> main.doubleit() ./main.go:7 (hits goroutine(1):1 total:1) (PC: 0x49c8bb)
     2:	package main
     3:
     4:	import "fmt"
     5:
     6:	func doubleit(val int) int {
=>   7:		return val * 3 // should be * 2
     8:	}
     9:
    10:	func main() {
    11:		fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12:		fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv) args
val = 2
~r0 = 0

So in this point in the program we’re in the doubleit method when it was invoked with a val value of 2. We can print this and other variables using p:

(dlv) p val
2

We can even call methods from this point using call:

(dlv) call doubleit(6)
> main.doubleit() ./main.go:7 (hits goroutine(6):1 total:2) (PC: 0x49c8bb)
     2:	package main
     3:
     4:	import "fmt"
     5:
     6:	func doubleit(val int) int {
=>   7:		return val * 3 // should be * 2
     8:	}
     9:
    10:	func main() {
    11:		fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12:		fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv) args
val = 6

In the example above we hit our own breakpoint set earlier allowing us to print the args for the call.

To walk over the execution we can press n to go to the next point of execution:

(dlv) n
> main.doubleit() ./main.go:6 (PC: 0x49c8a0)
     1:	// main.go
     2:	package main
     3:
     4:	import "fmt"
     5:
=>   6:	func doubleit(val int) int {
     7:		return val * 3 // should be * 2
     8:	}
     9:
    10:	func main() {
    11:		fmt.Printf("doubleit 2: %d\n", doubleit(2))
(dlv) n
> main.doubleit() ./main.go:7 (hits goroutine(6):2 total:3) (PC: 0x49c8bb)
     2:	package main
     3:
     4:	import "fmt"
     5:
     6:	func doubleit(val int) int {
=>   7:		return val * 3 // should be * 2
     8:	}
     9:
    10:	func main() {
    11:		fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12:		fmt.Printf("doubleit 4: %d\n", doubleit(4))
(dlv) n

and view the backtrace with bt:

(dlv) bt
0  0x000000000049c8bb in main.doubleit
   at ./main.go:7
1  0x000000000046251f in debugCall256
   at :0
2  0x0000000000407484 in runtime.debugCallWrap2
   at /usr/local/go/src/runtime/debugcall.go:251
3  0x00000000004073b3 in runtime.debugCallWrap1
   at /usr/local/go/src/runtime/debugcall.go:203
4  0x0000000000464ca1 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:1594

To view the code around the current point of execution just press l:

(dlv) l
> main.doubleit() ./main.go:7 (hits goroutine(6):3 total:4) (PC: 0x49c8bb)
     2:	package main
     3:
     4:	import "fmt"
     5:
     6:	func doubleit(val int) int {
=>   7:		return val * 3 // should be * 2
     8:	}
     9:
    10:	func main() {
    11:		fmt.Printf("doubleit 2: %d\n", doubleit(2))
    12:		fmt.Printf("doubleit 4: %d\n", doubleit(4))

Which of course shows our very hard to find logic error, * 3 instead of * 2.

Note: you can also set breakpoints in the stdlib functions (paths will vary depending on your setup):

(dlv) b src/net/http/request.go:899
Breakpoint 1 set at 0x794599 for net/http.NewRequestWithContext() /usr/local/go./net/http/request.go:899

summary

The example above is extremely trivial - where dlv and it’s ilk shine are on complex usecases where you may not even know what methods are between the input and output, such as debugging the stdlib. Just this week I used dlv to identify why a POST wasn’t honouring a 307 temporary redirect - on inspection, using dlv, I learned that the body is disregarded if it is an unrecognised type https://github.com/golang/go/blob/master/src/net/http/request.go#L899. Having to do this without a debugger would have taken quite a bit of code hopping - the debugger took care of that for me and allowed me to validate argument values as I did it.

why not gdb?

I know some folk feel strongly that gdb is the tool to use for debugging go code, but given the Golang docs itself encourage you to use Delve over GDB I personally stay away from it:

Note that Delve is a better alternative to GDB when debugging Go programs built with the standard toolchain. It understands the Go runtime, data structures, and expressions better than GDB. Delve currently supports Linux, OSX, and Windows on amd64. For the most up-to-date list of supported platforms, please see the Delve documentation.

Exceptions here may be usage of cgo but I’ll leave that out for now.

source: https://tip.golang.org/doc/gdb