Wednesday, April 12, 2023

C-style print debug in golang


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"
$
        
  
The 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