Developers who write c/c++ or rust should be familiar with conditional compilation. As the name suggests, conditional compilation means that a part of the code takes effect or fails during compilation, thereby controlling the code execution path at compile time, and thus affecting the behavior of compiled programs.
What's the use of this? It is usually useful when writing cross-platform code. For example, I want to develop a file operation library, which has a unified interface for the entire platform. However, the files and file system APIs provided by major operating systems are blooming. We cannot use just one set of code to make our library run normally on all operating systems.
At this time, conditional compilation is required. On Linux, we only allow the code that is adapted to Linux to take effect, and on Windows, we only allow the code related to Windows to take effect and other invalidation. for example:
#ifdef _Windows typedef HFILE file_handle #else typedef int file_handle #endif file_handle open_file(const char *path) { if (!path) { #ifdef _Windows return invalid_handle; #else return -1; #endif } #ifdef _Windows OFSTRUCT buffer; return OpenFile(path, &buffer, OF_READ); #else return open(path, O_RDONLY|O_CLOEXEC); #endif }
In this example, the APIs of Windows and Linux are completely different. In order to hide this difference, we use conditional compilation to define a set of the same interfaces on different platforms, so we don't need to care about platform differences.
From the above example, we can also see that the most commonly used method for c/c++ to implement conditional compilation is to rely on macros. By specifying the platform-specific identity at compile time, these precompiled macros can automatically remove unnecessary code and not compile. Another way to implement conditional compilation in c and c++ is to rely on the build system. We no longer use precompiled macros, but we will write a copy of code for each platform:
// open_file_windows.c typedef HFILE file_handle file_handle open_file(const char *path) { if (!path) { return invalid_handle; } OFSTRUCT buffer; return OpenFile(path, &buffer, OF_READ); } // open_file_linux.c typedef int file_handle file_handle open_file(const char *path) { if (!path) { return -1; } return open(path, O_RDONLY|O_CLOEXEC); }
Then specify that the build system only uses open_file_linux.c when compiling Linux programs, and only uses open_file_windows.c on Windows. This can also exclude incompatible codes that are not related to the current platform. Current construction systems such as meson and cmake can easily implement the above functions.
Golang, which claims to be system-level, naturally supports conditional compilation, and the way it supports is to rely on the second type - that is, rely on building systems.
There are two ways to use conditional compilation in golang. Because we do not use macros and cannot specify information to go build at compile time which codes are not needed, we need some means to let the go compile toolchain identify the code that should be compiled and ignored.
The first one is to rely on file suffix names. The name of the source code file of go is specified. Files that meet the following format will be considered as files that need to be compiled on a specific platform:
name_{system}_{arch}.go name_{system}_{arch}_test.go
The value of the system is the same as the environment variable GOOS. Common ones include windows, linux, darwin, and unix. When the suffix is unix, the file will be compiled on Linux, bsd and darwin platforms. If there is no explicit specification, the file will be valid on the entire platform unless there is an additional specification of the build tag we will talk about later.
The value of arch is the same as the GOARCH environment variables, and is both common hardware platforms such as amd64, arm64, loong64, etc. Files with these suffixes will only take effect and join the compilation process when the program is compiled for a specific hardware platform. If arch is not specified explicitly, this file will participate in compilation on all supported hardware platforms of the default target operating system.
The first method is simple and easy to understand, but the disadvantages are also obvious. We need to maintain a source code file for each platform, and there must be a lot of duplicate platform-independent code in these files, which is a big burden for maintenance.
Therefore, the first solution is only suitable for code with huge differences between platforms. A typical example is Go's own runtime code. Because coroutine scheduling requires many functions of operating systems and even hardware platforms to assist, runtime has a big difference outside of its own API on each operating system, so it is more appropriate to use the file name suffix to divide it into multiple files for maintenance.
The second method no longer uses filename suffixes, but relies on build tags to prompt the compiler which code needs to be compiled.
build tag is a compilation directive of go, which tells the compiler under what conditions the file needs to be compiled:
//go:build expression
Tags are generally written at the beginning of the file (after the copyright notice). The expressions are some tag names and simple boolean operators. for example:
:build !windows
Indicates that the file is compiled on a system other than Windows
:build linux && (arm64 || amd64)
It means that this file is only compiled on an Linux system with arm64 or amd64.
:build ignore
Special tags, which means that the file will be ignored on any platform, unless you explicitly use go run, go build or go generate to run the file.
:build Custom tag name
It means that this file is compiled only if the same tag name is specified by `go build -tags tag name`
The value of the predefined tag is actually the system and arch mentioned in the file name suffix. You can see that both logical operators and brackets can be used, and the semantics are the same as logical operations. The advantage of using tags is that it allows common logic for Linux and Windows to appear in the same file without copying two copies into _windows.go and _linux.go. More importantly, it allows us to customize the compiled tags.
If you can customize tags, there are many ways to play. Let's take a look at an example. A toy program that can specify the log output level at compile time. Its characteristic is that logs below the specified level will not only not output, but also will not even exist in code, which will truly achieve zero overhead.
This is usually done by controlling the log output level:
func DebugLog(msg ...any) { if level > DEBUG { return } ... } func InfoLog(msg ...any) { if level > INFO { return } ... }
However, this inevitably requires an if judgment. If the function is more complicated, it will also require an additional function call overhead.
Using conditional compilation eliminates these overheads, first of all, dealing with debug-level log functions:
// file log_debug.go //go:build debug || (!info && !warning) package log import "fmt" func Debug(msg any) { ("DEBUG:", msg) } // file log_no_debug.go //go:build info || warning package log func Debug(_ any) {}
As the lowest level, it will only take effect if the debug tag is specified and by default. Other times are empty functions.
The processing of the info level is the same, and it will only take effect when the specified levels are debug and info:
// file log_info.go //go:build !debug && !warning package log import "fmt" func Info(msg any) { ("INFO:", msg) } // file log_no_info.go //go:build warning package log func Info(_ any) {}
Finally, there is the warning level, where the logs at this level will be output no matter when, so it does not require conditional compilation and does not require tags:
// file log_warning.go package log import "fmt" func Warning(msg any) { ("WARN:", msg) }
Finally, the main function:
package main import "conditionalcompile/log" func main() { ("A debug level message") ("A info level message") ("A warning level message") }
Because we write all the functions that do not take effect as empty functions, the compiler will find that the calls of these empty functions do nothing during compilation, so they are directly ignored, so there will be no additional overhead when running.
Here is a simple test:
$ go run
#Output
DEBUG: A debug level message
INFO: A info level message
WARN: A warning level message
$ go run -tags info .
#Output
INFO: A info level message
WARN: A warning level message
$ go run -tags warning .
#Output
WARN: A warning level message
Consistent with what we expected. However, I do not recommend you to use this method because it requires writing two copies of code for each log function, and requires very complex logical operations on the compiled tag, which is very prone to errors; and a if judgment at runtime generally does not bring too much performance overhead. Unless it is clearly positioned to determine that the log level has an unacceptable performance bottleneck, don't try to use the above toy code.
However, there are really examples of using custom tags in production practice: wire.
Dependency injection tool Wire allows developers to write dependencies that need to be injected into source files with special compiled tags. These source files will not be compiled into the program when they are compiled normally. These files will only be recognized when using the wire tool to generate injected code. This can not only implement the dependency injection function normally without having too much impact on the code. For more specific methods, you can see the Wire usage tutorial.
As for choosing which method to implement conditional compilation in golang, this must be based on actual needs. At least, the code of go itself, as well as the file name suffix and build tag in k8s, are used in parallel. The most important basis for choosing is to facilitate yourself and others to maintain the code.
This is the introduction to this article about detailed explanation of the examples of conditional compilation in golang. For more relevant go conditional compilation content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!