Nils in Go

May 22, 2016 at 2:50PM
Caleb Doxsey

Most popular programming languages allow variables to take a special null value, which indicates that the variable is unset, uninitialized, undefined or not truly a value of a given type.

For example, in Python you use None:

def example():
  x = None
  print("x =", repr(x)) # x = None

Ruby has nil:

def example
  x = nil
  print("x = ", x.inspect) # x = nil
end

For some reason Javascript has two nulls: undefined, and null:

function example() {
  var x;
  console.log("x =", x); // x = undefined
  x = null;
  console.log("x =", x); // x = null
}

And C, C++, Java and C# all have nullable pointers. In the case of Java and C# almost everything can be set to null:

using System;

class Example {
}

class MainClass {
  public static void Main (string[] args) {
    Example x = null;
    Console.WriteLine("x = ", x);
  }
}

Though both also support non-nullable basic types. For example int: (and C# lets you define your own struct types)

class MainClass {
  public static void Main(string[] args) {
    int x = 0; // x cannot be assigned null
    Console.WriteLine("x = ", x);
  }
}

Go has nil. Pointers, functions, interfaces, slices, channels and maps can all be assigned the special, untyped, global value nil:

func main() {
  var xs *int = nil
  fmt.Println(xs) // nil
}

nil is also the default, zero value for any unitialized variables of these types:

// all nil
var (
  v1 *int
  v2 func()
  v3 interface{}
  v4 []int
  v5 chan int
  v6 map[int]int
)

Dangerous Nils

One of the downsides of having a null value is it's a special case that programmers often fail to account for. NullReferenceExceptions happen all the time in day-to-day Java or C# development. Indeed this is a frequent complaint about Go - since they were creating a brand new language, why didn't the designers fix Hoare's famous billion dollar mistake and get rid of nil entirely?

I won't levy a defense of null. It does cause a lot of headaches. Though I will mention in passing, Chesterton's famous fence:

In the matter of reforming things, as distinct from deforming them, there is one plain and simple principle; a principle which will probably be called a paradox. There exists in such a case a certain institution or law; let us say, for the sake of simplicity, a fence or gate erected across a road. The more modern type of reformer goes gaily up to it and says, “I don’t see the use of this; let us clear it away.” To which the more intelligent type of reformer will do well to answer: “If you don’t see the use of it, I certainly won’t let you clear it away. Go away and think. Then, when you can come back and tell me that you do see the use of it, I may allow you to destroy it. GK Chesterton

I'm probably just as guilty of this, but all to often we criticize language design without really understanding what would happen if it were actually changed.

Instead of arguing about whether or not nil should even exist in Go, I thought it'd be worthwhile to go over some practical ways nil can be used more safely in Go. But before looking at those, let's clarify why null fails. Since this isn't just a Go problem, I will use C# for a couple examples.

Classes define the fields and methods for an object and member access is done using the . operator. When we define a variable we use our class as the type and hand it a value. One of the allowed values for a variable is null. But since null doesn't actually represent an object of the given class any attempt at accessing its fields will result in a null reference exception:

using System;

class Example {
  public int x;
}

class MainClass {
  public static void Main(string[] args) {
    Example ex = null;
    Console.WriteLine(ex.x); // results in System.NullReferenceException
  }
}

The same is true for methods:

using System;

class Example {
  public int x() {
    return 1;
  }
}

class MainClass {
  public static void Main(string[] args) {
    Example ex = null;
    Console.WriteLine(ex.x()); // results in System.NullReferenceException
  }
}

The reason this method call fails is because of dynamic dispatch and the fact that there is no virtual method table available for null. There are actually ways to deal with these problems in C#, namely null-conditional operators: ex.?x instead of ex.x, and extension methods:

class Example {}

static class ExampleExtensions {
  public static int x(this Example ex) {
    return 1;
  }
}

class MainClass {
  public static void Main(string[] args) {
    Example ex = null;
    Console.WriteLine(ex.x()); // this works just fine
  }
}

But how do we solve these problems in Go?

1. Use Go Design Patterns

Functional programming languages avoid nil by using Option types. For example Rust has enum Option { None, Some(T) }:

fn main() {
  // This function uses pattern matching to deconstruct optionals
  fn compute(x: Option) -> String {
    match x {
      Some(a) => format!("The value is: {}", a),
      None    => format!("No value")
    }
  }
  println!("{}", compute(Some(42))); // The value is: 42
  println!("{}", compute(None)); // No value
}

The Option type forces you to deal with the possibility of nil because Option doesn't have any of the methods or fields you'd like to get access to, only the inner T type does.

Go doesn't have an Option type, and without generics you can't really implement one, but it does have some design patterns which basically serve the same purpose.

By convention, if a Go function can fail in some way, rather than return a single result, it will return two results, either the initialized value and a boolean:

x := map[int]int{ 1: 2 }
y, ok := x[2] // y == 0, ok == false
z, ok := x[1] // z == 2, ok == true

Or the initialized value and an error:

f, err := os.Open("somefile")
// on success, f is non-nil and err is nil
// on error, f is nil and err is non-nil

Though this convention is less elegant than using the type system, it is so widely used that its even enshrined and enforced using tools like golint.

In practice, this means that accessing fields or methods on a nil value returned by a method is an easy mistake to avoid. To most moderately experienced Go developers ignoring errors just looks wrong:

f, _ := os.Open("somefile")
defer f.Close()
// do stuff with f

And since f is only ever nil if an error is returned, handling errors means you avoid null reference panics.

2. Avoid Pointers in Fields

Pointers in Go are much more explicit than they are in Java or C#. When you see a plain, user-defined type in those languages it almost always means you are dealing with a reference type, and therefore, in reality, a pointer.

For example suppose we had a Person class:

class Person {
  Address address;
}

class Address {
  String street;
}

Given Person p = new Person();, p.address.street will result in a null reference exception because the address field is actually a pointer. The equivalent Go code would be:

type Person struct {
  address *Address
}

type Address struct {
  street string
}

But we can make the code safer by removing the inner pointer:

type Person struct {
  address Address
}

This should dramatically reduce the number of cases where null references can happen.

3. Add Methods for Field Access

When pointers are required you can add a getter method which returns a default value when the field is unset. For example:

type Persion struct {
  address *Address
}

func (p *Person) GetAddress() *Address {
  if p.address == nil {
    return new(Address)
  }
  return p.address
}

This is what the protobuf library does (at least in version 2), though this solution isn't particularly idiomatic, and I would opt for just avoiding pointers on fields when possible.

4. Make Nil Implement the Interface

As it turns out Go's methods are a lot closer to C#'s extension methods than the virtual methods in more typical object oriented programming languages. Go doesn't have inheritance, so there's no way to override a method, and therefore, at least when not using interfaces, there's no need for a virtual method dispatch table. You always know precisely which function is called when you invoke a method on a variable. As a consequence of this you can call methods on nil without causing a panic:

package main

import "fmt"

type Example struct {}

func (ex *Example) x() int {
  return 1
}

func main() {
  fmt.Println((*Example)(nil).x()) // no panic!
}

(*Example)(int) is a bit clunky, so why didn't I do this:

func main() {
  fmt.Println(nil.x())
}

The compiler helpfully explains:

./main.go:12: use of untyped nil

As it turns out, nil is a polymorphic value, just like integer literals:

// 1 is also untyped so that you can use it for ints or floats
var x int     = 1
var y float64 = 1

However unlike 1, which will default to an int if there's no other type hint available, you can't use an untyped nil:

x := 1   //   valid: x is an int
y := nil // invalid: use of untyped nil

So that's why it's necessary for us to convert the untyped nil into a *Exampleish nil. This has interesting consequences when paired with interfaces.

In Go an interface is a set of methods attached to some concrete type. For example take the io.Reader interface:

type Reader interface {
  Read(p []byte) (n int, err error)
}

This interface is implemented by many types, including an *os.File, because it has a Read method with the same signature:

func (*File) Read

That means we can have a variable of type io.Reader and assign it a value of type *os.File, and when we call the Read method, it calls the underlying Read method on *os.File:

f, _ := os.Open("somefile")
var rdr io.Reader = f
rdr.Read(make([]byte, 10)) // this calls f.Read

So what happens if we assign nil to rdr?

var rdr io.Reader = nil
rdr.Read(make([]byte, 10))

This results in:

panic: runtime error: invalid memory address or nil pointer dereference

Which means nil actually is dangerous when invoking methods, albeit only when using an interface type. At least in this example... But there's a workaround. Since nil is polymorphic, what kind of nil are we dealing with in this example?

func main() {
	var rdr io.Reader = nil
	fmt.Printf("this nil is a %T", rdr)
}
this nil is a

Well that's strange... The docs explain why this happens:

If i is a nil interface value, TypeOf returns nil.

So this kind of nil is special because it's a nil interface value. If, however, we can coerce our nil into a more concrete typed nil, we can get different behavior. Let's make an io.Reader that is always empty:

type nilReader struct {}

func (rdr *nilReader) Read(p []byte) (n int, err error) {
  return 0, io.EOF
}

Now let's make an nilReaderish nil and store that in our rdr:

func main() {
  var rdr io.Reader = (*nilReader)(nil)
  rdr.Read(make([]byte, 10)) // this works
}

And there you have it. We have created a nil which is safe to call methods on. Here is a more complete example of what it would look like:

package main

import (
  "fmt"
  "io"
  "os"
)

type nilReader struct {}

func (rdr *nilReader) Read(p []byte) (n int, err error) {
  return 0, io.EOF
}

var safeNil *nilReader

func thingWhichReturnsAReader() (io.Reader, error) {
  return safeNil, fmt.Errorf("some error")
}

func main() {
  // suppose we ignored the error for some reason
  rdr, _ := thingWhichReturnsAReader()
  io.Copy(os.Stdout, rdr) // despite my screwup, no panic
}

But not so fast... there's a big downside to this approach. Although (*nilReader)(nil) == nil is true, (io.Reader)((*nilReader)(nil)) == nil is false, so if a user relied on a test to see if the reader was nil, it would always say it wasn't nil, even though the underlying, dynamic type is in fact nil.

5. Don't Use Nil at All

Because of the nil != nil problem you're probably better off just using an empty struct if you want to be extra safe. The above example can be rewritten like this:

package main

import (
  "fmt"
  "io"
  "os"
)

type emptyReader struct {}

func (rdr emptyReader) Read(p []byte) (n int, err error) {
  return 0, io.EOF
}

var empty emptyReader

func thingWhichReturnsAReader() (io.Reader, error) {
  return empty, fmt.Errorf("some error")
}

func main() {
  // suppose we ignored the error for some reason
  rdr, _ := thingWhichReturnsAReader()
  io.Copy(os.Stdout, rdr) // despite my screwup, no panic
}

Which should have the same zero-space, zero-allocation behavior as using a nil pointer.

Conclusion

So there are some ways to make use of nil safer in Go programs. There's actually another way to solve this problem related to multiple values and errors which I'll dive into more next time. Stay tuned.