In Go, sometimes nil is not nil!
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.
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!