Debugging Go Code With Delve
“Genius is 1% talent and 99% hard work.”
“e = mc^2”
– Albert Einstein
In the field of software development, the quotes can be changed slightly:
“Software development is 1% programming and 99% debugging.”
“errors = more code ^ 2”
– Some senior developer, probably
Debugging is something all developers must go through, and it does not care about your expertise. It’s a frustrating process. To err is human, and errors will absolutely creep into your program. I don’t know why, but errors are your spawns just like your code, so you are responsible for handling them.
We all love to print debug…
If you are a computer science undergrad, then you probably had a bad experience with a project that doesn’t seem to work the way you want it to. This would also apply to other developers as well. My code doesn’t work, and I don’t understand why.
I don’t know why this is the case, but there is a universal tendency for developers to gravitate towards print debugging. At the part of the code where you assume the error to be, you would print the input and output variables.
Print debugging is often faster for fixing simple bugs. The most important part of debugging is to check whether you are feeding the right input to a function and whether the function is spitting out the desired output. Print statements can handle this easily. However, there are some nasty bugs that tend to manifest in one function but originate from a distant function. There are also cases where you may want to check the call stack, track the lifespan of a variable, etc.
Trust me, you will run into a situation where you feel like you are wasting a lot of time typing print statements. Your code will look ugly, and you will get lost in your own code. Those are the times when you wish you knew how to use a debugger.
If you aren’t convinced, try thinking of it like this. It’s good to know how to use debuggers. I think that using print debugging because you don’t know how to use a debugger is bad practice. Like any skill, not knowing shouldn’t be the main reason why you aren’t using/learning it. That’s like ordering takeout because you don’t know how to cook. Or not working out because you don’t know how to use the gym equipment. Or not studying because you don’t understand a concept… you get the point.
Using the Delve Debugger
There is a popular debugger called gdb that can debug Go code. However, the Go documentation does not recommend it and instead points you towards a better alternative: Delve.
Delve is like a toolbox that has a lot of tools to help you squash those nasty bugs. You don’t have to catch a cockroach with your bare hands when you can use a bait station. Delve provides you with bug sprays, bait stations, torches, etc. Delve is used to catch Go bugs specifically, and it is quite easy to use.
First, let’s install it. If you are running Go v1.16 or later (which you probably should), you can use the following line:
$ go install github.com/go-delve/delve/cmd/dlv@latest
Make sure you installed it by running this:
$ dlv version
Delve is used to debug the main package and tests. Unfortunately, it is rather limited in terms of debugging packages other than main
, because Delve needs a working executable of the program, which requires a main
package. Like me, if you are writing a library that does not have a main
package, you need to write tests for your code, then debug that instead.
Going over the commands I use most often, with an example code
This is the code we will use for this example.
package main
import (
"fmt"
"math"
)
func main() {
n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
mean1 := calcMean(n1)
mean2 := calcMean(n2)
fmt.Println("mean1:", mean1)
fmt.Println("mean2:", mean2)
}
func calcMean(nums []float64) float64 {
mean := 0.0
for _, num := range nums {
mean += num
}
mean /= float64(len(nums))
return mean
}
The code is really simple! The main
function defined two slices n1
and n2
and calls calcMean
to calculate each mean value. This code looks fine, and the compiler isn’t complaining. If there were any syntactical error, we won’t even be able to compile our code. However, this code actually has one small issue. Can you guess what it might be?
Well, let’s run the code and see what happens.
$ go run main.go
mean1: 0.3
mean2: NaN
Interesting. mean1
seems alright, but mean2
looks a bit weird. We want a number, not a NaN
value. Why is this happening? Let’s use the debugger to figure out what’s actually going on.
cd
into the directory that includes your main.go
file, then run this:
$ dlv debug
If you are debugging tests, you can cd
into the directory that holds your *_test.go
files, then run this:
$ dlv test
As far as I know, there isn’t a GUI frontend for Delve, so you need to use the command line to use Delve. This is fine because Delve has a friendly CLI that you can actually understand.
$ dlv debug
Type 'help' for list of commands.
(dlv)
You will see that we have entered the Delve interface. Now we can run Delve-specific commands that will help us fix this bug.
(dlv) break main.go:8
Breakpoint 1 set at 0x496732 for main.main() ./main.go:8
The first command is break
. It creates a breakpoint in your code. A breakpoint is a point in your code that you can stop the execution of. In this case, I set a breakpoint at line 8 in the file main.go
. It is useful to set breakpoints before a suspicious code. Usually, it is a function that accepts an input and returns an output. To go to the breakpoint, we can use the following command:
(dlv) continue
> main.main() ./main.go:8 (hits goroutine(1):1 total:1) (PC: 0x496732)
3: import (
4: "fmt"
5: "math"
6: )
7:
=> 8: func main() {
9: n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
10: n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
11:
12: mean1 := calcMean(n1)
13: mean2 := calcMean(n2)
continue
will, well, continue traversing the code until the next breakpoint is reached. If there are no breakpoints set, continue
will just run through the entire program, finishing the run. Here, continue will take us to line 8. Note that a breakpoint won’t actually run the specified line. Let’s go line by line now.
(dlv) next
> main.main() ./main.go:9 (PC: 0x496749)
4: "fmt"
5: "math"
6: )
7:
8: func main() {
=> 9: n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
10: n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
11:
12: mean1 := calcMean(n1)
13: mean2 := calcMean(n2)
14:
(dlv) next
> main.main() ./main.go:10 (PC: 0x4967fa)
5: "math"
6: )
7:
8: func main() {
9: n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
=> 10: n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
11:
12: mean1 := calcMean(n1)
13: mean2 := calcMean(n2)
14:
15: fmt.Println("mean1:", mean1)
We use the next
command to go to the next line. Here, we traverse through lines 9 and 10, where we define n1
and n2
. Let’s see how these slices are represented in our debugger.
(dlv) print n1
[]float64 len: 5, cap: 5, [0.1,0.2,0.3,0.4,0.5]
(dlv) print n2
Command failed: could not find symbol value for n2
We use the print
command to see the details of a variable. We see something odd there, however. print n1
works, but print n2
doesn’t. This is because when we are in a certain line, that line doesn’t get executed until we move on to the next line. n1
is defined in line 9, while n2
is defined in line 10. We are currently at line 10, which means that line 9 has been executed, but line 10 has not yet been. To fix this, we just need to go to the next line.
(dlv) next
> main.main() ./main.go:12 (PC: 0x4968a5)
7:
8: func main() {
9: n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
10: n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
11:
=> 12: mean1 := calcMean(n1)
13: mean2 := calcMean(n2)
14:
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
(dlv) print n1
[]float64 len: 5, cap: 5, [0.1,0.2,0.3,0.4,0.5]
(dlv) print n2
[]float64 len: 5, cap: 5, [NaN,0.2,0.3,0.4,0.5]
You can see how print n2
works as expected now.
We are at line 12 at the moment, and this is where using a debugger can differentiate itself from print debugging. Delve has a feature where you can step in to a function. As its name suggests, stepping into the function allows us to go to the function and examine that function step by step. I’ll show you what it means:
(dlv) step
> main.calcMean() ./main.go:19 (PC: 0x496ac0)
14:
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
18:
=> 19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
Use step
to step into a function. We can see how our scope changed from the main
function to calcMean
function.
(dlv) args
nums = []float64 len: 5, cap: 5, [...]
~r0 = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004074232364696
Using args
will show us the function parameters. nums
is the input for calcMean
. ~r0
denotes the return value of the function. r
stands for return, and 0
stands for the 0th return value. If we have multiple return values, we will have something like r0, r1, r2...
. The ~
represents an unnamed value. We know that calcMean
will return a float64
, but we didn’t specify a name for it.
We can check function input by using the print
command as well.
(dlv) print nums
[]float64 len: 5, cap: 5, [0.1,0.2,0.3,0.4,0.5]
So we know that there is nothing wrong with the input. Let’s keep going. We want to keep track of the value of the mean
variable. To do that, use the display
command.
(dlv) display -a mean
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:20 (PC: 0x496ae5)
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
=> 20: mean := 0.0
21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496aeb)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 0
Using the -a
flag will add variables to our display list. If we move on to the next line, you can see how the value of mean
shows at the bottom.
There is a small error that says how we could not find symbol value
. This is because mean
is not defined yet. You can see how this error goes away when we are at line 21 and mean
has been defined.
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 0
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 0.1
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 0.1
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 0.30000000000000004
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 0.30000000000000004
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 0.6000000000000001
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 0.6000000000000001
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 1
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 1
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 1.5
(dlv) next
> main.calcMean() ./main.go:24 (PC: 0x496b65)
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
22: mean += num
23: }
=> 24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 1.5
I apologize for the long output, but I really wanted to show you how mean
changes for every iteration. You can see that the mean is adding up.
(dlv) next
> main.calcMean() ./main.go:26 (PC: 0x496b87)
21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
=> 26: return mean
27: }
0: mean = 0.3
mean
is equal to 0.3 after the division, so the function works as expected for input of n1
. Let’s see what happens when we input n2
.
(dlv) next
> main.main() ./main.go:12 (PC: 0x4968c5)
Values returned:
~r0: 0.3
7:
8: func main() {
9: n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
10: n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
11:
=> 12: mean1 := calcMean(n1)
13: mean2 := calcMean(n2)
14:
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
0: mean = error could not find symbol value for mean
(dlv) display -d 0
(dlv)
We get out of the scope when we hit the return line. We need to remove mean
from the display list because we are not tracking it anymore. Run display -d
and follow it with an id of the variable in the list. In our case, it is 0.
(dlv) next
> main.main() ./main.go:13 (PC: 0x4968cb)
8: func main() {
9: n1 := []float64{0.1, 0.2, 0.3, 0.4, 0.5}
10: n2 := []float64{math.NaN(), 0.2, 0.3, 0.4, 0.5}
11:
12: mean1 := calcMean(n1)
=> 13: mean2 := calcMean(n2)
14:
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
18:
(dlv) step
> main.calcMean() ./main.go:19 (PC: 0x496ac0)
14:
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
18:
=> 19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
(dlv) print nums
[]float64 len: 5, cap: 5, [NaN,0.2,0.3,0.4,0.5]
Again, we step into the calcMean
function. We print nums
and check that the input is as expected.
dlv) display -a mean
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:20 (PC: 0x496ae5)
15: fmt.Println("mean1:", mean1)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
=> 20: mean := 0.0
21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
0: mean = error could not find symbol value for mean
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496aeb)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = 0
We use the display
command to keep track of mean
.
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = 0
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = NaN
(dlv) next
> main.calcMean() ./main.go:22 (PC: 0x496b44)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
21: for _, num := range nums {
=> 22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
27: }
0: mean = NaN
(dlv) next
> main.calcMean() ./main.go:21 (PC: 0x496b56)
16: fmt.Println("mean2:", mean2)
17: }
18:
19: func calcMean(nums []float64) float64 {
20: mean := 0.0
=> 21: for _, num := range nums {
22: mean += num
23: }
24: mean /= float64(len(nums))
25:
26: return mean
0: mean = NaN
And that, ladies and gentlemen, is where the bug is. Note how mean
becomes NaN
as soon as we add the first element of nums
. And from that point on, mean
stays NaN
no matter what we add to it. This is, obviously, an undesirable behavior. We want to ignore NaN values and calculate the mean without them.
Exit the debugger by using exit
.
Let’s go back to our code and update the calcMean
code to the following:
func calcMean(nums []float64) float64 {
mean := 0.0
nanCount := 0
for _, num := range nums {
if math.IsNaN(num) {
nanCount++
continue
}
mean += num
}
mean /= float64(len(nums) - nanCount)
return mean
}
If we encounter a NaN value, we increase nanCount
by 1, and skip to the next iteration by using continue
. When calculating the mean, we divide the sum of nums
by the length of nums
minus the number of NaNs.
Let’s see how our code runs now.
$ go run main.go
mean1: 0.3
mean2: 0.35
Nice! We successfully debugged our code.
Conclusion
We learned how to use Delve to debug our code like a pro. Of course, the above example could’ve been easily debugged by adding a print statement inside the loop in calcMean
. However, when you encounter nasty bugs in your journey, you will thank yourself for reading this post and knowing how to use a debugger. Delve provides many more commands, which you can check out in their documentation. Last time, we learned how to write tests to prevent bugs. When bugs do appear, we now know how to use a debugger to squash them.
Thank you for reading! You can also read this post on Medium and Dev.to.