Engineering Blog

                            

Returning a nil receiver

In this blog, we are going to discuss the impact of returning an interface and why doing so may lead to errors in some conditions. This mistake is probably one of the most widespread in Go because it may be considered counterintuitive, at least before we’ve made it.

What is the problem?

Let’s consider the following example. We will work on a Customer struct and implement a Validate method to perform sanity checks. Instead of returning the first error, we want to return a list of errors. To do that, we will create a custom error type to convey multiple errors:

type MultiError struct {
 errs []string
}

type error interface {
 Error() string
}

func (m *MultiError) Add(err error) {
 m.errs = append(m.errs, err.Error())
}

func (m *MultiError) Error() string {
 return strings.Join(m.errs, ";")
}

type Customer struct {
 Name string
 Age  int
}

MultiError satisfies the error interface because it implements Error() string. Meanwhile, it exposes an Add method to append an error. Using this struct, we can implement a Customer.Validate method in the following manner to check the customer’s age and name. If the sanity checks are OK, we want to return a nil error:

func (c Customer) Validate() error {
 var m *MultiError
 if c.Age < 0 {
  m = &MultiError{}
  m.Add(errors.New("age is negative"))
 }
 if c.Name == "" {
  if m == nil {
   m = &MultiError{}
  }
  m.Add(errors.New("name is nil"))
 }
 return m
}

Now, let’s test this implementation by running a case with a valid Customer:

func main() {
 customer := Customer{Age: 33, Name: "Hari"}
 if err := customer.Validate(); err != nil {
  log.Fatalf("customer is invalid: %v", err)
 }
}

The output is

2022/12/23 07:34:24 customer is invalid: <nil>

In Go pointer receiver can be nil

Let’s experiment by creating a dummy type and calling a method with a nil pointer receiver:

type Foo struct{}

func (foo *Foo) Bar() string {
return "bar"
}

func main() {
var foo *Foo
fmt.Println(foo.Bar())
}

foo is initialized to the zero value of a pointer: nil. But this code compiles, and it prints bar if we run it. A nil pointer is a valid receiver.

An interface is a dispatch wrapper

In the above example, m is initialized to the zero value of a pointer: nil. Then, if all the checks are valid, the argument provided to the return statement isn’t nil directly but a nil pointer. Because a nil pointer is a valid receiver, converting the result into an interface won’t yield a nil value. In other words, the caller of Validate will always get a non-nil error.

To make this point clear, let’s remember that in Go, an interface is a dispatch wrapper. Here, the wrappee is nil (the MultiError pointer), whereas the wrapper isn’t (the error interface)

Therefore, regardless of the Customer provided, the caller of this function will always receive . The error wrapper isn’t a non-nil error. Understanding this behavior is nil. imperative, because it’s a widespread Go mistake.

So, what should we do to fix this example?

The easiest solution is to return m only if it’s not nil:

func (c Customer) Validate() error {
 var m *MultiError
 if c.Age < 0 {
  m = &MultiError{}
  m.Add(errors.New("age is negative"))
 }
 if c.Name == "" {
  if m == nil {
   m = &MultiError{}
  }
  m.Add(errors.New("name is nil"))
 }
 if m != nil {
  return m
 }
 return nil
}

At the end of the method, we check whether m is not nil. If that is true, we return m; otherwise, we return nil explicitly. Hence, in the case of a valid Customer, we return a nil interface, not a nil receiver converted into a non-nil interface.

Conclusion

We’ve seen in this section that in Go, having a nil receiver is allowed, and an interface converted from a nil pointer isn’t a nil interface. For that reason, when we have to return an interface, we should return not a nil pointer but a nil value directly. Generally, having a nil pointer isn’t a desirable state and means a probable bug. We saw an example with errors throughout this section because this is the most common case leading to this error. But this problem isn’t only tied to errors: it can happen with any interface implemented using pointer receivers.

References:

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co

Previous Post
Next Post