DEV Community

Momchil Atanasov
Momchil Atanasov

Posted on • Updated on

Beware of Go interfaces

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)
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: You can run the program in the Go Playground.

The problem occurs on the following line:

var err error = typed
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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),
}
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

...and a caller that propagates the error implicitly.

func caller() error {
  result, err := doSomething()
  if err == nil {
    log.Println(result)
  }
  return err // implicit
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Lastly, avoid named result parameters.

func caller() (err error) {
  var result int
  result, err = doSomething() // implicit
  if err == nil {
    log.Println(result)
  }
  return // implicit
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (3)

Collapse
 
philipjohnbasile profile image
Philip John Basile

very helpful!

Collapse
 
hariskhan14 profile image
hariskhan14

Small correction: The approach that Go takes for interfaces is explicit, not implicit as defined in the first few lines.

Collapse
 
mokiat profile image
Momchil Atanasov • Edited

Hi, maybe I didn't explain it correctly. What I meant was that implementation of an interface is implicit. (i.e. there is no extends keyword in Go).

I am now looking at the official documentation:
go.dev/tour/methods/10

It seems to use a similar phrasing:

Interfaces are implemented implicitly

But maybe my sentence implies a different meaning? Will consider reworking it.

EDIT I have now added the implementation word in the sentence. Hopefully it reads correctly now. Thanks for pointing it out.