Skip to content

nil Pointer Error

Introduction

During a coding session, I needed to call the Close() method to close multiple objects, like in the code below:

go
type A struct {
  b B
  c C
  d D
}

func (a A) Close() error {
  if a.b != nil {
    if err := a.b.Close(); err != nil {
      return err
    }
  }

  if a.c != nil {
    if err := a.c.Close(); err != nil {
      return err
    }
  }

    if a.d != nil {
        if err := a.d.Close(); err != nil {
            return err
        }
    }

  return nil
}

But writing so many if statements felt inelegant. Since B, C, and D all implement the Close method, I thought it could be more concise. So I put them into a slice and used a loop:

go
func (a A) Close() error {
  closers := []io.Closer{
    a.b,
    a.c,
    a.d,
  }

  for _, closer := range closers {
    if closer != nil {
      if err := closer.Close(); err != nil {
        return err
      }
    }
  }
  return nil
}

This seemed better, so let's run it and see:

go
func main() {
  var a A
  if err := a.Close(); err != nil {
    panic(err)
  }
  fmt.Println("success")
}

The result was unexpected—it actually crashed. The error message indicated that a method cannot be called on a nil receiver. The if closer != nil in the loop seemed to have no filtering effect:

panic: value method main.B.Close called using nil *B pointer

The example above is a simplified version of a bug I encountered. Many beginners might make this kind of mistake at first. Let me explain what's happening.

Interface

In previous sections, it was mentioned that nil is the zero value for reference types, such as slices, maps, channels, functions, pointers, and interfaces. For slices, maps, channels, and functions, they can all be viewed as pointers pointing to concrete implementations.

But interfaces are different. An interface consists of two things: a type and a value.

When trying to assign nil to a variable, it fails to compile and shows the following message:

use of untyped nil in assignment

The content roughly indicates that a variable with value untyped nil cannot be declared. Since there's untyped nil, there must be typed nil, and this situation often occurs with interfaces. Consider a simple example:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(pa)
  fmt.Println(pa == nil)
}

Output:

<nil>
true
<nil>
false

The result is very strange. Clearly pa outputs nil, but it doesn't equal nil. We can use reflection to see what it actually is:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.TypeOf(pa))
  fmt.Println(reflect.ValueOf(pa))
}

Output:

<nil>
true
*int
<nil>

From the result, we can see it's actually (*int)(nil). This means pa stores the type *int, while its actual value is nil. When performing equality comparison on interface values, it first checks if their types are equal. If types are not equal, it directly determines they're not equal, then checks if values are equal. This interface comparison logic can be referenced in the cmd/compile/internal/walk.walkCompare function.

Therefore, if you want an interface to equal nil, both its value and type must be nil. This is because the type in an interface is actually also a pointer:

go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

If you want to bypass the type and directly check if its value is nil, you can use reflection. Here's an example:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.ValueOf(pa).IsNil())
}

Using IsNil(), you can directly check if its value is nil, avoiding the problem mentioned above. So in daily use, if a function's return value is an interface type and you want to return a zero value, it's best to return nil directly, not any concrete implementation's zero value. Even if it implements the interface, it will never equal nil, which could lead to the error in the example.

Summary

After solving the problem above, let's look at the following examples:

When a struct's receiver is a pointer receiver, nil is usable. Consider this example:

go
type A struct {

}

func (a *A) Do()  {

}

func main() {
  var a *A
  a.Do()
}

This code runs normally without null pointer errors.

When a slice is nil, you can access its length and capacity, and you can append elements to it:

go
func main() {
  var s []int
  fmt.Println(len(s))
  fmt.Println(cap(s))
  s = append(s, 1)
}

When a map is nil, you can still access it, but a nil map is read-only. Attempting to write will cause a panic:

go
func main() {
  var s map[string]int
  i, ok := s[""]
  fmt.Println(i, ok)
  fmt.Println(len(s))

  // Attempting to write causes panic
  s["a"] = 1 // panic: assignment to entry in nil map

}

These nil characteristics in the examples above might be confusing, especially for Go beginners. nil represents the zero value for the types mentioned above, which is the default value. Default values should exhibit default behavior, which is exactly what Go's designers intended: to make nil more useful rather than immediately throwing null pointer errors. This philosophy is also reflected in the standard library. For example, starting an HTTP server can be written like this:

go
http.ListenAndServe(":8080", nil)

We can directly pass a nil Handler, and the http library will use the default Handler to process HTTP requests.

TIP

If interested, you can watch this video Understanding nil - Gopher Conference 2016, which is very clear and easy to understand.

Golang by www.golangdev.cn edit