During the development process of any application, developers will always need to perform some kind of code debugging. PHP, Python, and most of the other dynamic languages, are able to be modified at runtime, as long as the modifications do not explicitly need to be compiled. We can easily print data in dynamic operating environments, outputting our changes and printing variable information directly. In Go, you can of course speckle your code with Println
s before-hand to display variable information for debugging purposes, but any changes to your code need to be recompiled every time. This can quickly become cumbersome. If you've programmed in Python or Javascript, you'll know that the former provides tools such as pdb and ipdb for debugging, and the latter has similar tools that are able to dynamically display variable information and facilitate single-step debugging. Fortunately, Go has native support for a similar tool which provides such debugging features: GDB. This section serves as a brief introduction into debugging Go applications using GDB.
GDB is a powerful debugging tool targeting UNIX-like systems, released by the FSF (Free Software Foundation). GDB allows us to do the following things:
- Initial settings can be customize according to the specific requirements of your application.
- Can be set so that the program being debugged in the developer's console stops at the prescribed breakpoints (breakpoints can be conditional expressions).
- When the program has been stopped, you can check its current state to see what happened.
- Dynamically change the current program's execution environment.
To debug your Go applications using GDB, the version of GDB you use must be greater than 7.1.
When compiling Go programs, the following points require particular attention:
- Using
-ldflags "-s"
will prevent the standard debugging information from being printed - Using
-gcflags "-N-l"
will prevent Go from performing some of its automated optimizations -optimizations of aggregate variables, functions, etc. These optimizations can make it very difficult for GDB to do its job, so it's best to disable them at compile time using these flags.
Some of GDB's most commonly used commands are as follows:
- list
Also used in its abbreviated form l
, list
is used to display the source code. By default, it displays ten lines of code and you can specify the line you wish to display. For example, the command list 15
displays ten lines of code centered around line 15, as shown below.
10 time.Sleep(2 * time.Second)
11 c <- i
12 }
13 close(c)
14 }
15
16 func main() {
17 msg := "Starting main"
18 fmt.Println(msg)
19 bus := make(chan int)
- break
Also used in its abbreviated form b
, break
is used to set breakpoints, and takes as an argument that defines which point to set the breakpoint at. For example, b 10
sets a break point at the tenth row.
- delete
Also used in its abbreviated form d
, delete
is used to delete break points. The break point is set followed by the serial number. The serial number can be obtained through the info breakpoints
command. Break points set with their corresponding serial numbers are displayed as follows to set a break point number.
Num Type Disp Enb Address What
2 breakpoint keep y 0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
breakpoint already hit 1 time
- backtrace
Abbreviated as bt
, this command is used to print the execution of the code, for instance:
#0 main.main () at /home/xiemengjun/gdb.go:23
#1 0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#2 0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#3 0x0000000000000000 in ?? ()
- info
The info
command can be used in conjunction with several parameters to display information. The following parameters are commonly used:
info locals
Displays the currently executing program's variable values
info breakpoints
Displays a list of currently set breakpoints
info goroutines
Displays the current list of running goroutines, as shown in the following code, with the *
indicating the current execution
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
Abbreviated as p
, this command is used to print variables or other information. It takes as arguments the variable names to be printed and of course, there are some very useful functions such as $len() and $cap() that can be used to return the length or capacity of the current strings, slices or maps.
- whatis
whatis
is used to display the current variable type, followed by the variable name. For instance, whatis msg
, will output the following:
type = struct string
- next
Abbreviated as n
, next
is used in single-step debugging to skip to the next step. When there is a break point, you can enter n
to jump to the next step to continue
- continue
Abbreviated as c
, continue
is used to jump out of the current break point and can be followed by a parameter N, which specifies the number of times to skip the break point
- set variable
This command is used to change the value of a variable in the process. It can be used like so: set variable <var> = <value>
Now, let's take a look at the following code to see how GDB is typically used to debug Go programs:
package main
import (
"fmt"
"time"
)
func counting(c chan<- int) {
for i := 0; i < 10; i++ {
time.Sleep(2 * time.Second)
c <- i
}
close(c)
}
func main() {
msg := "Starting main"
fmt.Println(msg)
bus := make(chan int)
msg = "starting a gofunc"
go counting(bus)
for count := range bus {
fmt.Println("count:", count)
}
}
Now we compile the file, creating an executable file called "gdbfile":
go build -gcflags "-N -l" gdbfile.go
Use the GDB command to start debugging :
gdb gdbfile
After first starting GDB, you'll have to enter the run
command to see your program running. You will then see the program output the following; executing the program directly from the command line will output exactly the same thing:
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]
Ok, now that we know how to get the program up and running, let's take a look at setting breakpoints:
(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
In the above example, we use the b 23
command to set a break point on line 23 of our code, then enter run
to start the program. When our program stops at our breakpoint, we typically need to look at the corresponding source code context. Entering the list
command into our GDB session, we can see the five lines of code preceding our breakpoint:
(gdb) list
18 fmt.Println(msg)
19 bus := make(chan int)
20 msg = "starting a gofunc"
21 go counting(bus)
22 for count := range bus {
23 fmt.Println("count:", count)
24 }
25 }
Now that GDB is running the current program environment, we have access to some useful debugging information that we can print out. To see the corresponding variable types and values, type info locals
:
(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int
To let the program continue its execution until the next breakpoint, enter the c
command:
(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
After each c
, the code will execute once then jump to the next iteration of the for
loop. It will, of course, continue to print out the appropriate information.
Let's say that you need to change the context variables in the current execution environment, skip the process then continue to the next step. You can do so by first using info locals
to get the variable states, then the set variable
command to modify them:
(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
Finally, while running, the program creates a number of goroutines. We can see what each goroutine is doing using info goroutines
:
(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()
From the goroutines
command, we can have a better picture of what Go's runtime system is doing internally; the calling sequence for each function is plainly displayed.
In this section, we introduced some basic commands from the GDB debugger that you can use to debug your Go applications. These included the run
, print
, info
, set variable
, continue
, list
and break
commands, among others. From the brief examples above, I hope that you will have a better understanding of how the debugging process works in Go using the GDB debugger. If you want to get more debugging tips, please refer to the GDB manual on its official website.
- Directory
- Previous section: Error handling
- Next section: Write test cases