I have recently started programming in Go but have quite a bit of experience in other low-level languages such as C/C++, Rust, and Zig. These languages provide conditional compilation such as C/C++ #ifdef
, Rust macros, or Zig's comp time.
Take what I say with a grain of salt as I might be wrong but I have searched a lot without much luck. Go seems to be considerably more clunky though because you can only use one build tag per file so the most basic things require multiple build files instead of inline conditionals; for example, if I want to have verbose printing in debug mode (not using a logger, I'm aware they exist, I need this for dev use only) or a tag to enable extra assertions when doing simulation testing/fuzzing.
My current approach is to create 2 files similar to:
flag_enabled.go
//go:build flag
package foo
func bar(params ...any){
//do something
}
flag_disabled.go
//go:build !flag
package foo
func bar(params ...any){} //do nothing
Maybe I'm a noob but I haven't figured out how to move these to another package without changing bar
to Bar
(public) so the main directory becomes extremely messy as I add more conditional flags.
The only other approach I found is to use const
s and override them: -ldflags="-X foo.flag=true"
but I don't love that either.
Is there a better way to have conditional code compilation? If not, is there a cleaner way to use build flags (without creating 1000 files)? Is using ldflags
the more standard approach?
In short: There is only one way to achieve conditional code compilation in GoLang. However, there are many different ways to leverage that mechanism; which way is the best way depends on the specifics of any particular use case.
It is worth highlighting that the approach to GoLang is not "clunkiness" arising from laziness or lack of consideration but a deliberate embodiment of GoLang's prioritisation of clarity and simplicity over complexity.
Other languages' sophisticated conditional compilation capabilities may offer greater flexibility but at the expense of a significant trade-off: complexity.
It is not uncommon to encounter a complex tree of nested and interdependent conditional compilation symbols when reading "portable" code in such languages; to read and understand it, you must read the code like a compiler, not like a human.
Assuming that the example in the question is representative, then to deal with that specific case and your problems with approaches you have considered:
You are correct that moving the bar
function to a package requires that the conditional function itself be exported. But this need not be problematic.
You could, for example, put that package in an internal
folder, which receives special treatment from the GoLang compiler.
Any packages contained within an internal
folder (at any location in a project structure) are not themselves exported by the module. i.e. other packages within the same module may import any internal
packages in that same module, but other modules cannot.
You could then have a non-conditional bar
function variable that is a reference to the Bar
function in the internal/conditional
package:
import "my/module/internal/conditional"
var bar = conditional.Bar
There is nothing special or significant about the use of the
conditional
name for the package; it is just an illustration and may not be the most appropriate choice of name in your real case.
Within the conditional
(or whatever) package, you can then conditionally compile the Bar
function as required, using the standard GoLang build tag mechanism.
It is difficult to say in the general case, but if the number of build flags has reached a point where managing them has become problematic, I would be inclined to look at how those build/why flags are being used and consider a different approach that didn't involve so much conditional compilation within a single project.
For example: Could packages be refactored as modules, perhaps isolating multiple conditional build flags within that module, which would be fewer than - and reducing - the total in the higher-order module/project.
Again, this is not the only way to reduce the number of build flags; it may not itself even be viable in a given project.
But there are almost certainly options.