gobuildcompilation

how can I use clean conditional compilation in Go?


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 consts 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?


Solution

  • Is there a better way to have conditional code compilation?

    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.

    Why is GoLang so "clunky"?

    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.

    The specific example in the question...

    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.

    Dealing with large number of build conditions

    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.