During the talk of proposals for new features for Go, such as simplifying error handling and generics, I found myself reaching for, and missing, a different tool. The proposals are not bad, but I use Go knowing that it is not an expressive language, and adjust accordingly. But working through a recent design, I found myself missing old-fashioned assertions. In particular, they can be used to document and enforce program invariants, such as preconditions or postconditions, in your code.
The fact that assertions are missing is not an oversight, the question is listed in the FAQ. The section is short enough to be worthwhile quoting:
Go doesn’t provide assertions. They are undeniably convenient, but our experience has been that programmers use them as a crutch to avoid thinking about proper error handling and reporting. Proper error handling means that servers continue to operate instead of crashing after a non-fatal error. Proper error reporting means that errors are direct and to the point, saving the programmer from interpreting a large crash trace. Precise errors are particularly important when the programmer seeing the errors is not familiar with the code.
We understand that this is a point of contention. There are many things in the Go language and libraries that differ from modern practices, simply because we feel it’s sometimes worth trying a different approach.
First, although Go “doesn’t provide assertions”, it does have assertions. The nil-pointer checks and out-of-bounds checks that are inserted by the compiler are assertions. These checks serve to enforce preconditions to those operations. Clearly, the language authors believe that assertions are useful, they just don’t trust programmers to use them properly. On the other hand, Go also provides
exceptions panics, but has successfully built a culture where they are not used very often. Perhaps the same could be done for assertions?
Second, the FAQ is correct that assertions should not replace error handling. For example, users will, from time to time, enter a negative number when a positive number is required, and an assertion rather than proper error handling and recovery is poor design (just a random example). The question, however, is should Go’s normal error handling be used to specify invariants? I’m not convinced that the answer is yes.
The reason I was reaching for assertions was that I was debugging the design of some multi-threaded code that also used CGO. If certain invariants were not respected at key points in the program, handling the error was not really an option. I needed not only to document the invariants, but having them enforced in code helped with debugging. In the end, recovery was never going to be an option. If the coordination on your multithreaded code is broken, then how much of your state are you going to trust? Crashing is likely your best option.
Not that the distinction is always perfectly clear, but:
An assertion should document a predicate that is true by design. If the predicate is false, then there is a bug in the program, and that bug must be fixed.
A run-time error may or may not occur during any invocation of a function (or application), and conveys information about the program’s environment. No bug is implied.
Fortunately, writing assert functionality for Go is very easy. I wrote the assert package, where the key function is all of 5 lines long. There are similar packages in the ecosystem, but they are hard to find among all of the packages tied to testing. For all its simplicity, the resulting behaviour is better than C, as the panic will provide a complete stack trace if, on the very rare occasion, you have a bug. So, although Go doesn’t provide assertions, it does have assertions, and so can you.