Give Codeship’s CI/CD Platform a Try

Want to learn more?

Troubleshooting an application can be fairly complex, especially when dealing with highly concurrent languages like Go. It can be fairly simple to add print statements to determine subjective application state at specific intervals, however it’s much more difficult to respond dynamically to conditions developing in your code with this method.

Debuggers provide an incredibly powerful troubleshooting mechanism. Adding code for troubleshooting can subtly affect how an application runs. Debuggers can give a much more accurate view of your code in the wild.

A number of debuggers exist for Go, some inject code at compile time to support an interactive terminal which negates some of the benefit of using a debugger. The gdb debugger allows you to inspect your compiled binaries, provided they were linked with debug information, without altering the source code. This is quite a powerful feature, since you can pull a build artifact from your deployment pipeline and debug it interactively. You can read more about this in the official Golang docs, so this guide will give a quick overview into the basic usage of the gdb debugger for Go applications.

There seem to have been a number of changes in gdb since this was announced, most notably the replacement of -> operator with . for accessing object attributes. Keep in mind that there may be subtle changes like this between versions of gdb and Go. This guide was written using gdb version 7.7.1 and go version 1.5beta2.

Getting Started with gdb Debugging

To experiment with gdb I’m using a test application, the complete source code for which can be found on in gdb_sandbox on Github. Let’s start with a really simple application:

Let’s debug the application. First, build the Go binary and then execute gdb with the binary path as an argument. Depending on your setup, you’ll also need to load Go runtime support via a source command. At this point we’ll be in the gdb shell, and we can set up breakpoints before executing our binary.

First off, let’s put a breakpoint (b) inside the for loop and take a look at what state our code has in each loop execution. We can then use the print (p) command to inspect a variable from the current context and the list (l) and backtrace (bt) commands to take a look at the code around the current step. The application execution can be stepped using next (n) or we can just continue to the next breakpoint (c).

Let’s say we need to debug the value of y. We can start by setting a breakpoint where y is being set and then step through the code. Using info args we can check function parameters, and as before bt gives us the current backtrace.

Since we’re in a condition where the value of y is being set based on the function f, we can set out of this function context and examine code farther up the stack. While the application is running we can set another breakpoint at a higher level and examine the state there.

If we continue at this point we will bypass breakpoint 1 in function f, and we will trigger the breakpoint in the handleNumber function immediately since the function f is only executed for every second value of i. We can avoid this by disabling breakpoint 2 temporarily.

We can also clear and delete breakpoints using clear and delete breakpoint NUMBER respectively. By dynamically creating and toggling breakpoints we can efficiently traverse application flow.

Slices and Pointers

Few applications will be as simple as pure numerical or string values, so let’s make the code a little more complex. By adding a slice of pointers to the main function and storing generated pairs, we could potentially plot them later.

This time around let’s examine the slice or pairs as it gets built. First of all we’ll need to examine the slice by converting it to an array. Since handleNumber returns a *pair type, we’ll need to dereference the pointers and access the struct attributes.

You’ll notice that while gdb does identify the fact that pairs is a slice, we can’t directly access attributes. In order to access the members of the slice we need to convert it to an array via pairs.array. We can check the length and capacity of the slice though:

(gdb) p $len(pairs)
$12 = 3
(gdb) p $cap(pairs)
$13 = 4

At this point we can run the loop a few times and monitor the increasing value of x and y across different members of the slice. Something to note here is that struct attributes can be accessed via the pointer, so p pairs.array[2].y works just as well.

Goroutines

Now that we can access structs and slices, let’s make the application even more complicated. Let’s add some goroutines into the mix by updating our main function to process each number in parallel and pass the results back through a channel:

If we wait for the WaitGroup to complete and inspect the resulting pairs slice, we can expect the contents to be exactly the same, although perhaps in a different order. The real power of gdb here comes from inspecting goroutines in flight:

The first thing we do here is list all running goroutines and identify one of our handlers. We can then view a backtrace and essentially send any debug commands to that goroutine. The backtrace and listing clearly do not match, how backtrace does seem to be accurate. info args on that goroutine shows us local variables, as well as variables available in the main function, outside the scope of the goroutine function which are prepended with an &.

Conclusion

When it comes to debugging applications, gdb can be incredibly powerful. This is still a fairly fresh integration, and not everything works perfectly. Using the latest stable gdb, with go1.5beta2, many things are still broken:

Interfaces

According to the go blog post, go interfaces should be supported, allowing you to dynamically cast then to their base types in gdb. This seems to be broken.

Interface{} types

There is no current way to convert an interface{} to its type.

Listing a different goroutine

Listing surrounding code from within another goroutine causes the line number to drift, eventually resulting in gdb thinking the current line is beyond the bounds of the file and throwing an error:

Goroutine debugging is unstable

Handling goroutines general tends to be unstable; I managed to cause a number of segfaults executing simple commands. At this stage you should be prepared to deal with some issues.

Configuring gdb with Go support can be troublesome

Running gdb with Go support can be troublesome, getting the right combination of paths and build flags, and gdb auto-loading functionality doesn’t seem to work correctly. First of all, loading Go runtime support via a gdb init file initializes incorrectly. This may need to be loaded manually via a source command once the debugging shell has been initialized as described in this guide.

When should I use a debugger?

So when is it useful to use gdb? Using print statement and debugging code is a much more targeted approach.

When changing the code is not an option.

When debugging a problem where the source is not known, and dynamic breakpoints may be beneficial.

When working with many goroutines where the ability to pause and inspect program state would be beneficial.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles. Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.

We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.