Golang debugging
Table of Contents
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