DEV Community

Cover image for Zero cost debug assertions in Go
Alexandre Negrel
Alexandre Negrel

Posted on • Edited on • Originally published at negrel.dev

Zero cost debug assertions in Go

NOTE: You can find original post on my personal blog https://www.negrel.dev/blog/zero-cost-debug-assertions-in-go/index.html

While I was working on my very own
TUI library, I had this idea of bringing debug assertions to Go.

From dart.dev:

During development, use an assert statement—
assert(<condition>, <optionalMessage>); —to disrupt normal execution if a boolean condition is false.

In production code, assertions are ignored, and the arguments to assert aren’t evaluated.

Many languages such as Rust, C and Zig have assertions built-ins, but Go doesn’t.

I don’t know the exact reason, maybe they’re not considered idiomatic but they’re definitely useful.

Assertions really shine when you want to enforce an invariant that can’t be encoded in the type system. They’re not mean to replace runtime errors but programming errors such as passing a nil pointer to a function that always expect a valid pointer.

How are assertions implemented?

Typically, assertions are implemented using either macros or conditional compilation.

Macros are great as they can print friendly, easy to read failure descriptions with the failing expression and its location (line number). This is how C assert works.

int even_num = 1;
assert(even_num % 2 == 0);
Enter fullscreen mode Exit fullscreen mode

Will print:

Assertion `even_num % 2 == 0' failed.
Aborted
Enter fullscreen mode Exit fullscreen mode

Another way is to use conditional compilation and include different assert implementation at build time. Go supports conditional compilation via build constraints. Similar to //go:embed file.txt, //go:generate ... and //go:noinline constraints, //go:build ... can be used to include or exclude content of a file depending on go build tags specified.

Building an assert like function is trivial:

// debug.go
//go:build debug

package assert

func Assert(cond bool, message string) {
    if !cond {
        panic(message)
    }
}
Enter fullscreen mode Exit fullscreen mode
// prod.go
//go:build !debug
// Note the "!" before debug.

package assert

func Assert(_ bool, _ string) {
    // noop
}
Enter fullscreen mode Exit fullscreen mode

If a program importing the above assert package is build using go build -tags debug, panicking assert implementation will be included.

Voilà! Our implementation is now fully functional.

Going further…

While our simple implementation works, it sucks from a DX point of view as we can’t know asserted value unless it is specified in the message.

There are 2 ways to work around this limitation:

  • Generate code for each assertion
  • Create multiples specialized assert functions for specific usage

Generating code is generally the recommended way to tackle this kind of problem.
Nevertheless, it adds complexity to the build process that may not be worth it.
Personally, I would strongly reconsider using assertions as it adds an external tool with its own set of problems.

Creating multiple specialized assert functions seems more appealing as users of the package can import it like a regular Go package. This option can also improve code readability with descriptive function name such as assert.LessThan.

This is the solution I chose for my assert package which is a fork of the well-known testify package.

Gopher dropping the mic

Top comments (0)