In Go, sometimes nil is not nil!

Jason Lui
Xendit Engineering
Published in
4 min readSep 22, 2022

Type is life in Go, especially in interfaces, hence nil may not be nil at times.

When asked “what’s the most notorious pitfall in Go”, you’d probably answer for range variables; however, nil comparisons could also surprise you! Let’s inspect a use case.

A common task

Here’s an everyday DB task — start a transaction, do some update, then commit the transaction. During the update, you expect something to go wrong, e.g., the record is not found, or the input doesn’t match the schema, hence some custom error handling is necessary. Let’s say you want to inform the handlers the HTTP response code to return, like the following snippet shows.

The whole flow looks like the following.

Feel free to run it on https://go.dev/play/p/Vub8qzzka1Y, but before you do, guess what’d be printed!

What went wrong?

Did you expect it to print err updating while all functions/methods return nil as errors? Here’s why.

First of all, error is an interface in Go. As the official FAQ points out, interfaces are implemented as a type T and value V, like in this diagram.

interface schematic

In line 38 above (if err = txn.doUpdate(); err != nil {), we are actually assigning the return of txn.doUpdate(), which is of type “pointer of MyError”, to err, which is of interface error. This compiles because the “pointer of MyError” indeed implements the interface error, however, when assigned to that interface, it looks like

An interface is nil only when both T and V are unset. I don’t have any official reference for the reason behind this, but I don’t think this behavior is unexpected — an interface has two “fields”; hence it’s nil only if both are unset.

As the type of err is set, it’s never nil.

The fix

The quickest way to fix the above is to redeclare err in line 38 so that it becomes if err := txn.doUpdate(); err != nil (notice the colon, this means to declare another variable err who is only visible in this if block). This way, because err is of type *MyError (inferred from the right hand side), err != nil is no longer an interface comparison, but rather only checking if it’s a nil pointer of type MyError.

However, that is not robust because there is hardly any way you could remind the callers to redeclare the result of doUpdate() method. Instead, the error interface should always be returned. To access the custom fields in any custom errors, errors.As() should be used, like

The full, properly fixed code looks like

Feel free to play with it at https://go.dev/play/p/oSEUbKtUAH_u, see what happens when you return different errors in doUpdate() method!

Is it just with errors?

The example I gave was about errors, but unfortunately that’s just probably one interface you’d come across most often. Here’s an example with another one.

Let’s say we are building a configurable user authentication app. There are “features” that are supposed to extract different attributes of a user, which are of different data types. For example, when an admin uses a feature called “is_bot”, there should be logic to return a bool indicating if the user is a bot; a feature called “age” would return an integer, etc.

Now I want to have a test that asserts all “features” returned by the function AllFeatures() correspond to a specific function. Is the following implementation correct?

(boolRegistry returns a func func(user) bool while intRegistry returns a func func(user) int)

Please try out the full code at https://go.dev/play/p/tLiL_XVOb90! Try adding features without the corresponding func, see if the test fails.

With the explanation above, you should be able to unravel this mystery, feel free to PR/raise issues in https://github.com/mrkagelui/interface-example to discuss this further!

To recap, an interface in Go has two fields, T (Type) and V (Value), and it is nil only when both are unset. If you assign a concrete value, even a nil pointer, to it, it will not be nil. Be careful when you assign and compare them!

--

--