Debugging is an important part of software development.
One common technique for debugging is to add print statements to the code that output information about the state of the program.
In C, it's common to use preprocessor macros to conditionally compile these print statements so that they can be removed from the code when they're no longer needed. In this article, I'll show you how to achieve the same effect in Go.
Let's start with a simple example. Consider the following Go program: This program calculates the sum of a and b and prints the result to the console.
Now let's say we want to add some debug output to this program to help us understand what's happening.
We could add some
fmt.Println
statements like this:
This will print the values of a, b, and sum to the console before printing the final result. However, we might not always want this debug output to be present in our code. We could comment out the
fmt.Println
statements, but this can be tedious and error-prone. Instead, we can use a technique similar to C's preprocessor macros to conditionally compile these debug statements. A possible solution would be to define a function that will output our debug message if a given flag is set, but this would result in some unwanted call and if statements evaluations. The key to avoid this is to use a constant declaration to control whether the debug messages are compiled or not. Here's an example: and In Go,
if
a condition inside an if statement always evaluates to a constant value, the compiler can optimize
the code and remove all the contents of the if statement. This can lead to a function becoming empty, and eventually, the function call being removed altogether as well.
$ cat debug.go
package main
import (
"fmt"
"runtime"
)
const (
debugNone = 0
debugIO = 1
debugAddFunctionName = 15
)
const DebugLevel uint32 = debugIO | debugAddFunctionName
func debugIOPrintf(format string, a ...interface{}) (int, error) {
var s string
var n int
var err error
if DebugLevel&(1<<(debugIO-1)) != 0 {
if DebugLevel&(1<<(debugAddFunctionName-1)) != 0 {
pc, _, _, ok := runtime.Caller(1)
s = "?"
if ok {
fn := runtime.FuncForPC(pc)
if fn != nil {
s = fn.Name()
}
}
newformat := "[" + s + "] " + format
n, err = fmt.Printf(newformat, a...)
} else {
n, err = fmt.Printf(format, a...)
}
return n, err
}
return 0, nil
}
$ go build
$ ./example3
[main.add] input a=1, b=2
[main.add] output sum=3
3
$ go tool objdump example3 | grep -E "main.debugIOPrint"
TEXT main.debugIOPrintf(SB) /home/alessandro/go/src/cstyle_print_go/example3/debug.go
debug.go:16 0x47f846 e975ffffff JMP main.debugIOPrintf(SB)
main.go:8 0x47f8d8 e8e3feffff CALL main.debugIOPrintf(SB)
main.go:10 0x47f925 e896feffff CALL main.debugIOPrintf(SB)
$ cat debug.go
package main
import (
"fmt"
"runtime"
)
const (
debugNone = 0
debugIO = 1
debugAddFunctionName = 15
)
const DebugLevel uint32 = debugNone
func debugIOPrintf(format string, a ...interface{}) (int, error) {
var s string
var n int
var err error
if DebugLevel&(1<<(debugIO-1)) != 0 {
if DebugLevel&(1<<(debugAddFunctionName-1)) != 0 {
pc, _, _, ok := runtime.Caller(1)
s = "?"
if ok {
fn := runtime.FuncForPC(pc)
if fn != nil {
s = fn.Name()
}
}
newformat := "[" + s + "] " + format
n, err = fmt.Printf(newformat, a...)
} else {
n, err = fmt.Printf(format, a...)
}
return n, err
}
return 0, nil
}
$ go build
$ ./example3
3
$ go tool objdump example3 | grep -E "main.debugIOPrint"
$
go tool objdump example3 | grep -E "main.debugIOPrint"
command can be used to search for function calls in a Go binary.
If the DebugLevel
constant is set to debugNone
, this command won't find any occurrences of the debugIOPrint
function call.
This illustrates how a function call can be optimized out by the Go compiler, making it possible to use such functions as replacements for C-style macros in Go. Here the link to the repo that hosts this example.
No comments:
Post a Comment