Last time we shared the method of debugging Go code using assembly language. Assembly language allows us to easily trace low-level knowledge of the Go runtime and other underlying details. In this article, we will introduce the powerful debugging tool called “go tool”, which, when mastered, can elevate your development skills to the next level.
This article focuses on practical techniques for debugging in Golang and the effective usage of related tools, so you no longer need to worry about how to debug Golang code. Golang, as a modern language, comes with built-in debugging capabilities from the very beginning:
- Golang tools are directly integrated into the language tools, supporting memory analysis, CPU analysis, and blocking lock analysis, among others.
- Delve and GDB are the most commonly used debug tools, allowing you to dive deeper into program debugging.
- Delve is currently the most user-friendly Golang debugging program, and IDE debugging is essentially calling dlv, such as in Goland.
- Unit testing is deeply integrated into the language design, making it very convenient to execute unit tests and generate code coverage.
Golang tools
Golang integrates a variety of useful tools at the language level, which are the essence of the experience accumulated by Robert Griesemer, Rob Pike, Ken Thompson, and other experts. After installing Golang, you can see all the built-in tools by executing go tool
.
|
|
Here, I will focus on selecting several commonly used debug tools:
nm
: view the symbol table (equivalent to the systemnm
command).objdump
: disassembly tool, used to analyze binary files (equivalent to the systemobjdump
command).pprof
: metric and performance analysis tool.cover
: code coverage generation.trace
: sampling over a period of time, metric tracking and analysis tool.compile
: code assembly.
Now, I will demonstrate the usage of these tools with an example.
|
|
nm
The nm
command is used to view the symbol table, which is equivalent to the system nm
command and is very useful. When setting breakpoints, if you don’t know the function symbol of the breakpoint, you can use this command to find out (this command operates on binary program files).
compile
Assemble a specific file:
|
|
You will be able to see the assembly code corresponding to your Go code (please note that this command operates on Go code text), which is cool.
objdump
The objdump tool is used to disassemble binaries, equivalent to the system objdump
(please note that this command parses binary program files).
|
|
Assembly code may not be needed in 90% of scenarios, but if you have experience working with C programs, in certain special situations, inferring application behavior by disassembling a piece of logic may be your only way out. This is because the code running in production usually has optimization enabled, which can cause your code to not match. Furthermore, you cannot attach to processes at will in production environments. Many times, you only have a core file to troubleshoot.
pprof
pprof supports four types of analysis:
CPU
: CPU analysis, sampling calls that consume CPU resources, which is generally used to locate and troubleshoot areas of high computational resource consumption in programs.Memory
: Memory analysis, which is generally used to troubleshoot memory usage, memory leaks, and other issues.Block
: Blocking analysis, which samples the blocking calls in the program.Mutex
: Mutex analysis, which samples the competition for mutex locks.
For more information about pprof, you can refer to this article.
trace
Program trace debugging:
|
|
The trace command allows you to trace and collect information over a period of time, then dump it to a file, and finally analyze the dump file using go tool trace
and open it in a web format.
Unit Testing
The importance of unit testing is self-evident. In Golang, files ending with _test.go
are considered test files. As a modern language, Golang supports unit testing at the language tool level.
Running Unit Tests
There are two ways to execute unit tests:
- Run
go test
directly, which is the simplest method. - Compile the test files first, then run them. This method provides more flexibility.
Running go test
|
|
Compilation and Execution
Essentially, running Golang unit tests involves compiling *_test.go
files into binaries and then running these binaries. When you execute go test
, the tool handles these actions for you, but you can also perform them separately.
Compile the test files to generate the test executable:
|
|
This method is usually used in the following scenarios:
- Compile on one machine and run tests on another.
- Debugging test programs.
Code Coverage Analysis
Golang’s code coverage is based on unit tests, which serve as the starting point for measuring code coverage of your business code. The operation is simple:
- Add the
-coverprofile
parameter when running tests to record code coverage. - Use the
go tool cover
command to analyze and generate coverage reports.
|
|
The output will be similar to the following:
|
|
This way, you can see the code coverage for each function.
Program Debugging
Program debugging mainly relies on two tools:
|
|
Here, I recommend dlv because GDB’s functionality is limited. GDB does not understand Golang’s specific types such as channels, maps, and slices. GDB’s native support for goroutines is limited since it only understands threads. However, GDB has one irreplaceable feature, which is the gcore
command.
dlv Debugging
Debugging Binaries
|
|
For example:
|
|
Debugging binaries with dlv and passing arguments:
|
|
Debugging Processes
|
|
The process ID is mandatory. For example:
|
|
Debugging Core Files
Debugging core files with dlv and redirecting standard output to a file:
|
|
|
|
Common Debugging Syntax
System Summary
Program Execution
- call: call a function (note that this will cause the entire program to run).
- continue: resume execution.
- next: step over.
- restart: restart.
- step: step into a function.
- step-instruction: step into a specific assembly instruction.
- stepout: step out of the current function.
Breakpoint-related
- break (alias: b): set a breakpoint.
- breakpoints (alias: bp): print all breakpoint information.
- clear: clear a breakpoint.
- clearall: clear all breakpoints.
- condition (alias: cond): set a conditional breakpoint.
- on: set a command to be executed when a breakpoint is hit.
- trace (alias: t): set a tracepoint, which is also a breakpoint but does not stop the program when hit; it only prints a line of information. This command is useful in certain scenarios where stopping the program affects logic (e.g., business timeouts), and you only want to print a specific variable.
Information Printing
- args: print function arguments.
- examinemem (alias: x): a powerful command for examining memory, similar to gdb’s
x
command. - locals: print local variables.
- print (alias: p): print an expression or variable.
- regs: print register information.
- set: set variable value.
- vars: print global variables (package variables).
- whatis: print type information.
Goroutine-related
- goroutine (alias: gr): print information of a specific goroutine.
- goroutines (alias: grs): list all goroutines.
- thread (alias: tr): switch to a specific thread.
- threads: print information of all threads.
Stack-related
- deferred: execute commands in the context of a defer function.
- down: move up the stack.
- frame: jump to a specific stack frame.
- stack (alias: bt): print stack information.
- up: move down the stack.
Other Commands
- config: modify configurations.
- disassemble (alias: disass): disassemble.
- edit (alias: ed): omitted.
- exit (alias: quit | q): omitted.
- funcs: print all function symbols.
- libraries: print all loaded dynamic libraries.
- list (alias: ls | l): display source code.
- source: load commands.
- sources: print source code.
- types: print all type information.
The above commands cover the complete set of commands supported by dlv, which meet our debugging needs (some commands are only applicable during development and debugging, as it is not possible to single-step debug on production code in most cases).
Application Examples
Print Global Variables
|
|
This is very useful for inspecting global variables.
Conditional Breakpoints
|
|
Inspecting the Stack
|
|
Examining Memory
|
|
The x
command is the same as gdb’s x
command.
gdb Debugging
GDB’s support for Golang debugging is achieved through a Python script called src/runtime/runtime-gdb.py
, so its functionality is limited. GDB can only perform basic variable printing and cannot understand some of Golang’s specific types such as channels, maps, and slices. GDB cannot directly debug goroutines because it only understands threads. However, GDB has one feature that cannot be replaced, which is the gcore
command.
dlv Debugging Example
Debugging a Binary
|
|
Debugging a Process
|
|
Debugging a Core File
|
|
gdb Debugging Example
|
|
Print Global Variables (note the single quotation marks)
|
|
Due to GDB’s limited understanding of Golang’s type system, sometimes it may not be able to print variables, so please pay attention to this.
Print Array Length
|
|
Therefore, I usually only use GDB to generate core files.
Tips and Tricks
Don’t know how to set breakpoints in functions?
Sometimes you don’t know how to set breakpoints in a function. You can use nm
to query the function and then set a breakpoint, which will ensure that you hit the breakpoint.
Don’t know the calling context?
Add the following line in your code:
|
|
This will print the stack trace at the current code position, allowing you to understand the calling path of the function.
Don’t know how to enable pprof?
There are two ways to enable pprof, corresponding to two packages:
- net/http/pprof: used in web server scenarios.
- runtime/pprof: used in non-server applications.
These two packages are essentially the same, with net/http/pprof
being a web wrapper on top of runtime/pprof
.
Using net/http/pprof
|
|
Using runtime/pprof
This method is usually used for performance optimization. When running a program that is not a server application, you want to find bottlenecks, so you typically use this method.
|
|
Why does the code sometimes execute unexpectedly during single-step debugging?
This situation is usually caused by compiler optimization, such as function inlining and removal of redundant logic and parameters from the compiled binary. This can cause unexpected execution during dlv single-step debugging or prevent the printing of certain variables. The solution to this problem is to disable compiler optimization.
|
|
Conclusion
This article provides a systematic overview of the techniques and usage of Golang program debugging:
- The language tool package provides built-in tools that support assembly, disassembly, pprof analysis, symbol table queries, and other practical functions.
- The language tool package integrates unit testing, and code coverage relies on triggering unit tests.
- The powerful dlv/gdb tools serve as the main debugging tools, supporting the analysis of binaries, processes, and core files.