Momchil Atanasov
Posted on August 30, 2023
Interfaces play a huge part when working with the Go programming language. The implicit implementation approach that Go takes makes them very flexible and easy to write. But interfaces in Go have a dark side when dealing with nil
comparisons and this article aims to shine some light on it.
No time to waste, let's directly dive into it. Since error
is probably the most important and most commonly used interface in Go, it will be used in the examples that follow. However, the problem is applicable to any other interface type.
What just happened
Let's have a look at the following code.
package main
import (
"fmt"
"log"
)
type DivisionByZeroError struct {
Numerator int
Denominator int
}
func (e *DivisionByZeroError) Error() string {
return fmt.Sprintf("division by zero: %d / %d", e.Numerator, e.Denominator)
}
func divide(numerator, denominator int) (int, error) {
var (
result int
err *DivisionByZeroError
)
if denominator != 0 {
result = numerator / denominator
} else {
err = &DivisionByZeroError{
Numerator: numerator,
Denominator: denominator,
}
}
return result, err
}
func main() {
result, err := divide(10, 5)
if err != nil {
log.Fatalf("Division by zero: %v", err)
}
log.Printf("Answer: %d", result)
}
It might be a bit overengineered for a simple divide
function but it's all for demonstration purposes.
The important thing to note is that we have defined a custom type DivisionByZeroError
that implements the error
interface and is returned by divide
whenever one tries to divide by zero.
We run the program and since we divide 10
by 5
we expect to get Answer: 2
but instead we get the following error:
2009/11/10 23:00:00 Division by zero: <nil>
NOTE: You can run the program in the Go Playground.
This makes absolutely no sense. Not only did Go decide that err != nil
was true
, when it should have been false
, but it also printed <nil>
to the console, contradicting itself.
What is going on? Is this a compiler error? Was the custom error type incorrectly defined?
Simplified example
Well - no. The problem from above has to do with how Go converts from concrete types to interfaces and how it compares interfaces.
Here is a simplified example of the same problem.
package main
import (
"log"
)
type CustomError struct{}
func (e *CustomError) Error() string {
return "custom error"
}
func main() {
var typed *CustomError = nil
var err error = typed
if err != nil {
log.Fatalf("Error: %v", err)
}
}
NOTE: You can run the program in the Go Playground.
The problem occurs on the following line:
var err error = typed
This converts the nil
value of type *CustomError
to an interface of type error
. The problem is that the interface is not nil
, despite what one may think.
It prints as <nil>
to the console but this is only because it references a nil
value of *CustomError
but the interface itself is not nil
.
Inner workings
We have to look at how interfaces are represented internally to understand what is going on.
An interface in Go can be thought of as a struct that has two fields - a reference to the value and a reference to the type it represents.
type interface struct {
value *internal.Pointer
type *internal.Type
}
NOTE: This is an over-simplified Go-syntax representation, which acts only for demonstration purposes and does not accurately reflect the actual design.
When you assign a variable to an interface, the value
field holds a pointer to your variable and the type
field holds a reference to the Type definition of your type. That way, Go can know what methods the value has and how to cast it, if needed.
When you compare an interface in Go with nil
, Go checks that value
and type
are both nil
. If value
is nil
but type
is not, then the check will fail.
In our case, when we assign var err error = typed
, in essence we do var err error = (*CustomError)(nil)
.
This can be thought of as resulting in the following:
var err error = interface {
Value: nil,
Type: CustomError.(type),
}
NOTE: Again, this is total nonsense Go code but it should help you get a feel for how it works internally.
As explained above, since type
is not nil
, the check err == nil
returns false
, or vice versa, the check err != nil
returns true
, which happens to be our case.
But why
I have known of this "problem" for a long time now and I have long been asking myself why did the Go developers choose to implement interface comparison this way.
Why didn't they just check that the interface value
isn't nil
and ignore the type
?
Only recently did I put two and two together and figured out why this was the case. Up to that point, I always thought it was for legacy / backward compatibility reasons.
The thing is, nil
values in Go are actually usable. And since it will be hard to explain it in words, here is an example with our CustomError
from above.
package main
import (
"log"
)
type CustomError struct{}
func (e *CustomError) Error() string {
return "custom error"
}
func main() {
var err *CustomError = nil
log.Println(err.Error())
}
NOTE: You can run the program in the Go Playground.
You might expect the program to panic, since we are calling the Error
method on the err
value, which is nil
. However, instead, the program runs successfully and outputs the following to the console.
2009/11/10 23:00:00 custom error
Go allows you to call a method on a nil
value of a type. And as long as the implementation of the method does not dereference the receiver, nothing will panic. In fact, in our particular case we can define the Error
method of CustomError
in this simplified form - without a receiver variable name since we are not using the receiver in any way.
func (*CustomError) Error() string {
return "custom error"
}
What this means is that in some cases passing a nil
value of a type might actually be e valid / working implementation of some interface.
Hence, it would be incorrect from Go to treat the interface as nil
(unusable), since in fact it is perfectly valid, even if there is no actual value behind it and only a type.
Now what
Ok, so now that we know why Go compares interfaces with nil
in such a way, what can we do to prevent the problem we had from the first example?
An easy solution is to never implicitly pass nil
to an interface and instead always be explicit. That is, the original "divide" example would instead look as follows:
package main
import (
"fmt"
"log"
)
type DivisionByZeroError struct {
Numerator int
Denominator int
}
func (e *DivisionByZeroError) Error() string {
return fmt.Sprintf("division by zero: %d / %d", e.Numerator, e.Denominator)
}
func divide(numerator, denominator int) (int, error) {
if denominator == 0 {
return 0, &DivisionByZeroError{ // explicit
Numerator: numerator,
Denominator: denominator,
}
}
return numerator / denominator, nil // explicit
}
func main() {
result, err := divide(10, 5)
if err != nil {
log.Fatalf("Division by zero: %v", err)
}
log.Printf("Answer: %d", result)
}
NOTE: You can run the program in the Go Playground.
The code became much cleaner. Since this is probably how one would have written it to begin with, one might assume that it would be hard to get into the problem from the initial example. But I have seen it in practice a number of times. Often it is not so trivial and has to do with more complex functions being chained.
Most often it starts with a function that returns a concrete error type instead of the error
interface...
func doSomething() (int, *CustomError) {
...
}
...and a caller that propagates the error implicitly.
func caller() error {
result, err := doSomething()
if err == nil {
log.Println(result)
}
return err // implicit
}
Instead, the caller
function should perform explicit error returns as follows.
result, err := doSomething()
if err != nil {
return err // explicit
}
log.Println(result)
return nil // explicit
And the doSomething
function should not have returned *CustomError
as a result parameter. Instead, if possible, it should have used the error
interface instead.
func doSomething() (int, error) {
...
}
Lastly, avoid named result parameters.
func caller() (err error) {
var result int
result, err = doSomething() // implicit
if err == nil {
log.Println(result)
}
return // implicit
}
They tend to get people in trouble when not careful.
Fin
If you got so far, thank you for following along. Hopefully, this article was helpful.
It does not aim to bash Go in any way. As has been shown, there is a very good reason for Go to compare interfaces in such a way. Instead, my goal is for this to be helpful to new Go developers that are still learning the language and spare them a painful debugging session.
Feel free to add details in the comments if you think there is something I got wrong or if there is something important I forgot to mention.
Happy Go coding!
Posted on August 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.