Debugging Go Applications using Delve
Need for a debugger
The simplest form of debugging in any programming language is by using print statements/logs and writing to standard out. This definitely works but becomes extremely difficult when the size of our application grows and the logic becomes more complex. Adding print statements to every code path of the application is not easy. This is where debuggers come in handy. Debuggers help us to trace the execution path of the program using breakpoints and a host of other features. Delve is one such debugger for Go. In this tutorial, we will learn how to Debug Go applications using Delve.
Installing Delve
Please ensure that you are inside a directory which doesn’t contain a go.mod file. I prefer my Documents
directory.
cd ~/Documents/
Next, let’s set the GOBIN
environment variable. This environment variable specifies the location where the Delve
binary will be installed. Please skip this step if you have the GOBIN
already set. You can check whether GOBIN
is set by running the command below.
go env | grep GOBIN
If the above command prints, GOBIN=""
, it means that GOBIN
is not set. Please run export GOBIN=~/go/bin/
command to set GOBIN.
Let’s add GOBIN
to the PATH
by running export PATH=$PATH:~/go/bin
In the case of macOS, the Xcode command line developer tools are needed to run Delve. Please run xcode-select --install
to install the command line tools. Linux users can skip this step.
Now we are set to install Delve
. Please run
go install github.com/go-delve/delve/cmd/dlv@latest
to install the latest delve. After running this command, please test your installation by running dlv version
. It will print the version of Delve on successful installation.
Delve Debugger
Version: 1.21.0
Build: $Id: fec0d226b2c2cce1567d5f59169660cf61dc1efe
Starting Delve
Let’s write a simple program and then start debugging it using Delve.
Let’s create a directory for our sample program using the following command.
mkdir ~/Documents/debugsample
Create a file main.go
inside the debugsample
directory we just created with the following contents.
1package main
2
3import (
4 "fmt"
5)
6
7func main() {
8 arr := []int{101, 95, 10, 188, 100}
9 max := arr[0]
10 for _, v := range arr {
11 if v > max {
12 max = v
13 }
14 }
15 fmt.Printf("Max element is %d\n", max)
16}
Also create a go module by running,
go mod init github.com/golangbot.com/debugsample
The program above will print the biggest element of the slice arr
. Running the above program will output,
Max element is 188
We are now ready to debug the program. Let’s move to the debugsample directory cd ~/Documents/debugsample
. After that, type the following command to start Delve.
dlv debug
The above command will start debugging the main
package in the current directory. After typing the above command, you can see that the terminal has changed to (dlv)
prompt. If you can see this change, it means that the debugger has started successfully and waiting for our commands :).
Let’s fire our first command.
In the dlv
prompt, type continue
.
(dlv) continue
The continue
command will run the program until there is a breakpoint or till completion of the program. Since we do not have any breakpoints defined, the program will run till completion.
Max element is 188
Process 1733 has exited with status 0
If you see the above output, the debugger has run and the program is completed :). But this was not of any use to us. Let’s go ahead and add a couple of breakpoints and watch the debugger do its magic.
Creating Breakpoints
Breakpoints pause the execution of the program at a specified line. When the execution is paused, we can send commands to the debugger to print the value of the variables, look at the stack trace of the program, and so on.
The syntax for creating a breakpoint is provided below,
(dlv) break filename:lineno
The above command will create a breakpoint at line lineno
in the file filename
.
Let’s add a breakpoint to line no. 9 of our main.go
.
(dlv) break main.go:9
When the above command is run, you can see the output,
Command failed: Process 25471 has exited with status 0
Set a suspended breakpoint (Delve will try to set this breakpoint when the process is restarted) [Y/n]?
Delve warns us that the program has exited and that it will try to set the break point when the process is restarted. This is because the program has exited when we ran continue
earlier since there were no breakpoints at that time. Type y
in this prompt to set the break point when the process is restarted.
Now let’s restart the program by typing restart
in the prompt.
(dlv) restart
Process restarted with PID 2028
The restart
command restarts the program.
Now let’s continue
our program and check whether the debugger pauses the program at the breakpoint.
(dlv) continue
> main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10c16e4)
4: "fmt"
5: )
6:
7: func main() {
8: arr := []int{101, 95, 10, 188, 100}
=> 9: max := arr[0]
10: for _, v := range arr {
11: if v > max {
12: max = v
13: }
14: }
After continue
is executed, we can see that the debugger has paused our program at line no 9. Just what we wanted :).
Listing breakpoints
(dlv) breakpoints
The above command lists the current breakpoints of the application.
(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x4379e0,0x4378e0 for (multiple functions)() <multiple locations>:0 (0)
Breakpoint unrecovered-panic (enabled) at 0x437da0 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1143 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x49c846 for main.main() ./main.go:9 (1)
You might be surprised to see that there are two other breakpoints in addition to the one we added. The other two breakpoints are added by delve to ensure that the debugging session does not end abruptly when there is a runtime panic that is not handled using recover.
Printing variables
The program’s execution has paused at line no. 9. print
is the command used to print the value of a variable. Let’s use print
and print the element at the 0th index of the slice arr
.
(dlv) print arr[0]
Running the above command will print 101
which is the element at the 0th index of the slice arr
.
Do note that if we try to print max
, we will get a error.
(dlv) print max
Command failed: could not find symbol value for max
This is because the program has paused before line no. 9 is executed and hence max
is not defined yet. To print the actual value of max, we should move to the next line of the program. This can be done using the next
command.
Move to next line in the source
(dlv) next
will move the debugger to the next line and it will output,
> main.main() ./main.go:10 (PC: 0x10c16ee)
5: )
6:
7: func main() {
8: arr := []int{101, 95, 10, 188, 100}
9: max := arr[0]
=> 10: for _, v := range arr {
11: if v > max {
12: max = v
13: }
14: }
15: fmt.Printf("Max element is %d\n", max)
Now if we try (dlv) print max
we can see the output 101
.
next command can be used to walk through a program line by line.
If you keep typing next
, you can see that the debugger walks you line by line in the program. When one iteration of the for
loop in line no. 10 is over, next
will walk us through the next iteration and the program will terminate eventually.
Printing expressions
print can also be used to evaluate expressions. For example, if we want to find the value of max + 10
, it’s possible using print.
Let’s add another breakpoint outside the for
loop where the computation of max
will be completed.
(dlv) break main.go:15
The above command adds another breakpoint to line no. 15 where the computation of max is finished.
Type continue
and the program will stop at this breakpoint.
print max+10
command will output 198.
Clearing breakpoints
clear is the command to clear a single breakpoint and clearall is the command to clear all breakpoints in the program.
Let’s first list the breakpoints in our application.
(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x4378e0,0x4379e0 for (multiple functions)() <multiple locations>:0 (0)
Breakpoint unrecovered-panic (enabled) at 0x437da0 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1143 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x49c846 for main.main() ./main.go:9 (1)
Breakpoint 2 (enabled) at 0x49c8e5 for main.main() ./main.go:15 (1)
We have two breakpoints named 1
and 2
If we run clear 1
, it will delete the breakpoint 1
.
(dlv) clear 1
Breakpoint 1 cleared at 0x10c16e4 for main.main() ./main.go:9
If we run clearall
, it will delete all breakpoints. We have only one breakpoint named 2
remaining.
(dlv) clearall
Breakpoint 2 cleared at 0x10c1785 for main.main() ./main.go:15
From the above output, we can see that the remaining one breakpoint is also cleared. If we executed continue
command now, the program will print the max
value and terminate.
(dlv) continue
Max element is 188
Process 3095 has exited with status 0
Step into and out of a function
It is possible to use Delve to step into a function or out of a function. Don’t worry if it doesn’t make sense now :). Let’s try to understand this with the help of an example.
1package main
2
3import (
4 "fmt"
5)
6
7func max(arr []int) int {
8 max := arr[0]
9 for _, v := range arr {
10 if v > max {
11 max = v
12 }
13 }
14 return max
15}
16func main() {
17 arr := []int{101, 95, 10, 188, 100}
18 m := max(arr)
19 fmt.Printf("Max element is %d\n", m)
20}
I have modified the program we have been using till now and moved the logic which finds the biggest element of the slice to its own function named max
.
Quit Delve using (dlv) q
, replace main.go
with the program above and then start debugging again using the command dlv debug
.
Let’s add a breakpoint at line. no 18 where the max
function is called.
b is the shorthand for adding a breakpoint. Let’s use that.
(dlv) b main.go:18
(dlv) continue
We have added the breakpoint at line no.18 and continued the execution of the program. Running the above commands will print,
> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
13: }
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
=> 18: m := max(arr)
19: fmt.Printf("Max element is %d\n", m)
20: }
The program execution has paused at line no. 18 as expected. Now we have two options.
- Continue debugging deeper into the
max
function - Skip the max function and move to the next line.
Depending on our requirement we can do either. Let’s learn how to do both.
First, let’s skip the max function and move to the next line. To do this, you can just run next
and the debugger will automatically move to the next line. By default, Delve doesn’t go deeper into function calls.
(dlv) next
> main.main() ./main.go:19 (PC: 0x10c17d3)
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
18: m := max(arr)
=> 19: fmt.Printf("Max element is %d\n", m)
20: }
You can see from the above output that the debugger has moved to the next line.
Type continue
and the program will finish executing.
Let’s learn how to go deeper into the max function.
Type restart
and continue
and we can see the program paused again at the already existing breakpoint.
(dlv) restart
Process restarted with PID 5378
(dlv) continue
> main.main() ./main.go:18 (hits goroutine(1):1 total:1) (PC: 0x10c17ae)
13: }
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
=> 18: m := max(arr)
19: fmt.Printf("Max element is %d\n", m)
20: }
Now type step
and we can see that the control has moved into the max
function now.
(dlv) step
> main.max() ./main.go:7 (PC: 0x10c1650)
2:
3: import (
4: "fmt"
5: )
6:
=> 7: func max(arr []int) int {
8: max := arr[0]
9: for _, v := range arr {
10: if v > max {
11: max = v
12: }
Type next
and the control will move to the first line of the max
function.
(dlv) next
> main.max() ./main.go:8 (PC: 0x10c1667)
3: import (
4: "fmt"
5: )
6:
7: func max(arr []int) int {
=> 8: max := arr[0]
9: for _, v := range arr {
10: if v > max {
11: max = v
12: }
13: }
If you keep typing next
you can step through the execution path of the max
function.
You might be wondering whether it is possible to return to main
without stepping through each line in the max
function. Yes, this is possible using the stepout
command.
(dlv) stepout
> main.main() ./main.go:18 (PC: 0x10c17c9)
Values returned:
~r1: 188
13: }
14: return max
15: }
16: func main() {
17: arr := []int{101, 95, 10, 188, 100}
=> 18: m := max(arr)
19: fmt.Printf("Max element is %d\n", m)
20: }
Once you type stepout
, the control returns back to main. Now you can continue debugging in main
:).
Printing stack trace
A very important functionality needed when debugging is to print the current stack trace of the program. This is useful to find out the current code execution path. stack
is the command used to print the current stack trace.
Let’s clear all breakpoints add a new breakpoint at line no. 11 and print the current stack trace of the program.
(dlv) restart
(dlv) clearall
(dlv) b main.go:11
(dlv) continue
When the program is paused at the breakpoint, type
(dlv) stack
It will output the current stack trace of the program.
0 0x000000000049c849 in main.max
at ./main.go:11
1 0x000000000049c933 in main.main
at ./main.go:18
2 0x000000000043a0d8 in runtime.main
at /usr/local/go/src/runtime/proc.go:250
3 0x0000000000464c21 in runtime.goexit
at /usr/local/go/src/runtime/asm_amd64.s:1594
So far we have covered basic commands to help start debugging your application using Delve. In the upcoming tutorials, we will cover advanced features of Delve such as debugging goroutines, attaching the debugger to an existing process, remote debugging and also using Delve from the VSCode editor.
I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.