You have already read about debugging in previous topics. Developers use this tool when their code doesn't run correctly. So what is debugging? In this topic, you will take a closer look at what it is and what tools are used for it.
What is debugging?
Once you've written your program, the first thing you try to do is compile the code and run your application. At this point, you may experience that your code does not compile. This may happen because you have made a number of mistakes. These errors are related to compile time. At this stage, the compiler will show all the errors and give them an exhaustive comment. And if you use an IDE, then the chances of making such mistakes are extremely small. The IDE usually highlights code that cannot be compiled.
However, now we would like to discuss another type of error. Let's say you've already fixed all compilation errors, and your code is running. But the result of the program surprises you – it is wrong! These errors are related to runtime.
To understand what went wrong, you need to know:
- current values of variables;
- which way the program was executed.
Let's take a look at some tools that can help with this.
Logging as a debugging tool
This debugging method is the easiest. Many developers use logging to debug their code. Usually, after debugging, developers remove all lines of code associated with such debugging.
Let's look at a simple example:
package main
import "fmt"
func main() {
var array = []int{1, 2, 3, 4, 5, 6}
for _, num := range array {
if isEven(num) {
fmt.Printf("number %d is even\n", num)
} else {
fmt.Printf("number %d is odd\n", num)
}
}
}
func isEven(num int) bool {
res := num % 2
if res == 1 {
return true
}
return false
}
This code determines if the number is even or not. You may have already noticed a bug in the function above. Of course, this is a synthetic example and the functions in your code will often be larger and more complex.
To see where you made a mistake, you can output some data into the console:
func isEven(num int) bool {
res := num % 2
if res == 1 {
fmt.Println(num, res, true)
return true
}
fmt.Println(num, res, false)
return false
}
5 1 true
Thus, at any time when the function is called, you can get:
- function arguments value;
- function result;
- function execution path.
However, the result obtained above is sometimes difficult to interpret. For example, if you need to keep track of more than one value or more than one function. In such cases, you can take advantage of all the formatted output features. So you can replace the code above with the following:
func isEven(num int) bool {
res := num % 2
if res == 1 {
fmt.Printf("num = %d, res = %d, return true\n", num, res)
return true
}
fmt.Printf("num = %d, res = %d, , return false\n", num, res)
return false
}
num = 5, res = 1, return true
Another way of logging with output to the console can be offered by the standard "log" package. It extends the capabilities of the "fmt" package. For example, you can see the log time and some other data. Let's look at the code below:
func isEven(num int) bool {
logger := log.New(os.Stdout, "isEven: ", log.Lshortfile+log.Ltime+log.Ldate)
res := num % 2
if res == 1 {
logger.Printf("num = %d, res = %d, return true\n", num, res)
return true
}
logger.Printf("num = %d, res = %d, return false\n", num, res)
return false
}
isEven: 2022/09/18 22:23:29 even.go:24: num = 1, res = 1, return true
You can get a wider logging functionality using third-party packages like zap, logrus, or others. They allow much more flexible customization of output data. You will consider these possibilities separately in other topics.
Delve as a common Go debug tool
Delve is a simple and, at the same time, effective command line utility. Let's see how it works and analyze the main functions.
Firstly, to use Delve, you need to install it. This is a third-party utility, completely written on Go. If you use Go version 1.16 and above, just enter in the terminal:
> go install github.com/go-delve/delve/cmd/dlv@latest
go: downloading github.com/go-delve/delve v1.9.1
...
After that, you can check the version set by your command:
> dlv version
Delve Debugger
Version: 1.9.1
The next important command is help. This way, you can find out all the available commands and their brief description.
> dlv help
Delve is a source level debugger for Go programs.
...
To launch the debugger, you can use one of the following commands:
> dlv debug [path to main package]
> dlv exec [path to executable file]
> dlv attach PID
debug– compiles your program and launches a debugger. If you run the command in the catalog with themainpackage, then you can not indicate the path to the package.exec– launches a compiled executable file. Optionally accepts the path to the file.attach– connects to the advanced process according to itsPID.
After starting the debug process, you will be prompted to input debug commands:
Type 'help' for list of commands.
(dlv)
The first thing the debugger offers to do is to see the entire list of commands. Let's try:
(dlv) help
As you can see, the commands are divided into groups. Also, there are aliases for some of the most commonly used commands. For example: print (alias: p) ----- Evaluate an expression
Let's look at some of the commands.
Code view commands
The following commands will display part of the program code:
(dlv) list ./even.go:5
(dlv) list main.main
(dlv) list main.main:10
As an argument, the list can accept:
- file name and displaced lines from the start of the file (available only in
debugmode when Delve is involved in the compilation of the program); - the name of the package and the name of the function within the package with an optional displacement parameter relative to this function.
Breakpoints
Breakpoints are special marks where the debugger will stop. The break command sets breakpoints:
(dlv) break main.main:10
Breakpoint 1 set at 0x4a64af for main.isEven() ./even.go:19
(dlv) break ./even.go:22
Breakpoint 2 set at 0x4a6520 for main.isEven() ./even.go:22
The first command sets the offset relative to the package and the function in the package, the second – relative to the beginning of the file. As a result of running the command, you can see:
- instruction address 0x4a64af;
- main.isEven() function;
- file and line number ./even.go:19.
breakpoints command prints out all set breakpoints:
(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x435260 for runtime.throw()
/snap/go/9952/src/runtime/panic.go:982 (0)
Breakpoint unrecovered-panic (enabled) at 0x435620 for runtime.fatalpanic()
/snap/go/9952/src/runtime/panic.go:1065 (0)
print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x4a64af for main.isEven() ./even.go:19 (0)
Breakpoint 2 (enabled) at 0x4a6520 for main.isEven() ./even.go:22 (0)
As you can see, there are two standard breakpoints in case of a panic or fatal error. Next, you can see all the breakpoints we set.
clear command removes the break-point by its number:
(dlv) clear 1
Breakpoint 1 cleared at 0x4a64af for main.isEven() ./even.go:19
(dlv) breakpoints
Breakpoint runtime-fatal-throw (enabled) at 0x435260 for runtime.throw()
/snap/go/9952/src/runtime/panic.go:982 (0)
Breakpoint unrecovered-panic (enabled) at 0x435620 for runtime.fatalpanic()
/snap/go/9952/src/runtime/panic.go:1065 (0)
print runtime.curg._panic.arg
Breakpoint 2 (enabled) at 0x4a6520 for main.isEven() ./even.go:22 (0)Code navigation commands
Using the following commands, you can control the steps of the program:
(dlv) continue
(dlv) next
(dlv) step
(dlv) stepout
continue – proceed to the next breakpoint, next – go to the next line of the code, step – go to the next step of the program, stepout – go to a higher function.
next and step also allow you to go through the functions of standard Go packages. In the example above, you can see the logger.Printf() function code.Variable view commands
The last block we'll look at are functions that display the values of variables at the program's breakpoint. To do this, you can go to some function. In this case, it is the isEven() function of the main package:
args displays all the arguments of the function that is currently performed:
(dlv) args
num = 1
~r0 = false
false). The value returned to the function is calculated at the time of exiting this function.The following command prints all local variables known at the time of execution. In this case, these are logger and res. Since logger does not belong to the main types of Go; you get its type in the output. You also get the address at which the value of the variable is stored, since *log.Logger is a linked type of data.
(dlv) locals
logger = (*log.Logger)(0xc0000600a0)
res = 1
print displays the value of the requested variable:
(dlv) print num
2
Keep in mind that you can only get the values of variables that are visible in the current scope. For example, if you try to get the value of the variable array, which is declared inside the main() function, while in the isEven() function, you will get the following output:
(dlv) print array
Command failed: could not find symbol value for array
In this case, being in the main function, you will receive the correct value:
(dlv) print array
[]int len: 6, cap: 6, [1,2,3,4,5,6]Conclusion
In this topic, you once again looked at what debugging is. In practical examples, you examined the following methods of debugging:
- debugging using an additional code to output variables to the console;
- debugging using the external debugger Delve.
Both of these methods are useful tools that developers use in their work. It should be noted that the first method requires changing the program code and new compilation. Whereas Delve can be used even in cases where the program is already launched, and you are not able to stop it.
And yet, most often, the debugger is used at the time of development. Therefore, the choice of the method of debugging the program is only your personal solution!