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
)
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?
Functional programming languages avoid nil
by using Option
types. For example Rust has enum Option
:
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.
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.
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.
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 *Example
ish 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 nilReader
ish 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
.
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.
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.