Lesson 27. Much ado about nil

After reading lesson 27, you’ll be able to

  • Do something with nothing
  • Understand the trouble with nil
  • See how Go improves on nil’s story

The word nil is a noun that means nothing or zero. In the Go programming language, nil is a zero value. Recall from unit 2 that an integer declared without a value will default to 0. An empty string is the zero value for strings, and so on. A pointer with nowhere to point has the value nil. And the nil identifier is the zero value for slices, maps, and interfaces too.

Many programming languages incorporate the concept of nil, though they may call it NULL, null, or None. In 2009, prior to the release of Go, language designer Tony Hoare gave a presentation titled “Null References: The Billion Dollar Mistake.” In his talk (see mng.bz/dNzX), Hoare claims responsibility for inventing the null reference in 1965 and suggests that pointers to nowhere weren’t one of his brightest ideas.

Note

Tony Hoare went on to invent communicating sequential processes (CSP) in 1978. His ideas are the basis for concurrency in Go, the topic of unit 7.

Nil is somewhat friendlier in Go, and less prevalent than in past languages, but there are still caveats to be aware of. Nil has some unexpected uses too, which Francesc Campoy talked about in his presentation at GopherCon 2016 (see www.youtube.com/watch?v=ynoY2xz-F8s), providing inspiration for this lesson.

Consider this

Consider representing a constellation, where each star contains a pointer to its nearest neighboring star. After the math is done, every star will point somewhere, and finding the nearest star becomes a quick pointer dereference away.

But until all the calculations are done, where should the pointers point? This is one situation where nil comes in handy. Nil can stand in for the nearest star until it’s known.

What is another situation where a pointer to nowhere could be useful?

27.1. Nil leads to panic

If a pointer isn’t pointing anywhere, attempting to dereference the pointer won’t work, as listing 27.1 demonstrates. Dereference a nil pointer, and the program will crash. As a rule, people tend to dislike apps that crash.

I call it my billion-dollar mistake.

Tony Hoare

Listing 27.1. Nil leads to panic: panic.go
var nowhere *int
fmt.Println(nowhere)          1

fmt.Println(*nowhere)         2

  • 1 Prints <nil>
  • 2 Panic: nil pointer dereference

Avoiding panic is fairly straightforward. It’s a matter of guarding against a nil pointer dereference with an if statement, as shown in the following listing.

Listing 27.2. Guard against panic: nopanic.go
var nowhere *int

if nowhere != nil {
    fmt.Println(*nowhere)
}

To be fair, programs can crash for many reasons, not only because of nil pointer dereferences. For example, divide by zero also causes a panic, and the remedy is similar. Even so, considering all the software written in the past 50 years, the number of accidental nil pointer dereferences could be fairly costly for users and programmers alike. The existence of nil does burden the programmer with more decisions. Should the code check for nil, and if so, where should the check be? What should the code do if a value is nil? Does all this make nil a bad word?

There’s no need to cover your ears or avoid nil altogether. In truth, nil can be quite useful, as the remainder of this lesson demonstrates. Additionally, nil pointers in Go are less prevalent than null pointers are in some other languages, and there are ways to avoid their use when appropriate.

Quick check 27.1

Q1:

What’s the zero value for the type *string?

QC 27.1 answer

1:

The zero value for a pointer is nil.

 

27.2. Guarding your methods

Methods frequently receive a pointer to a structure, which means the receiver could be nil, as shown in the following listing. Whether a pointer is dereferenced explicitly (*p) or implicitly by accessing a field of the struct (p.age), a nil value will panic.

Listing 27.3. Nil receivers: method.go
type person struct {
    age int
}

func (p *person) birthday() {
    p.age++                     1
}

func main() {
    var nobody *person
    fmt.Println(nobody)         2

    nobody.birthday()
}

  • 1 nil pointer dereference
  • 2 Prints <nil>

A key observation is that the panic is caused when the p.age++ line executes. Remove that line, and the program will run.

Note

Contrast this to the equivalent program in Java, where a null receiver will crash the program immediately when a method is called.

Go will happily call methods even when the receiver has a nil value. A nil receiver behaves no differently than a nil parameter. This means methods can guard against nil values, as shown in the following listing.

Listing 27.4. Guard clause: guard.go
func (p *person) birthday() {
    if p == nil {
        return
    }
    p.age++
}

Rather than check for nil before every call to the birthday method, the preceding listing guards against nil receivers inside the method.

Note

In Objective-C, invoking a method on nil doesn’t crash, but rather than call the method, it returns a zero value.

You decide how to handle nil in Go. Your methods can return zero values, or return an error, or let it crash.

Quick check 27.2

Q1:

What does accessing a field (p.age) do if p is nil?

QC 27.2 answer

1:

It panics, crashing the program, unless the code checks for nil before the field access.

 

27.3. Nil function values

When a variable is declared as a function type, its value is nil by default. In the following listing, fn has the type of a function, but it isn’t assigned to any specific function.

Listing 27.5. Function types that are nil: fn.go
var fn func(a, b int) int
fmt.Println(fn == nil)        1

  • 1 Prints true

If the preceding listing were to call fn(1, 2), the program would panic with a nil pointer dereference, because there’s no function assigned to fn.

It’s possible to check whether a function value is nil and provide default behavior. In the next listing, sort.Slice is used to sort a slice of strings with a first-class less function. If nil is passed for the less argument, it defaults to a function that sorts alphabetically.

Listing 27.6. A default function: sort.go
package main

import (
    "fmt"
    "sort"
)

func sortStrings(s []string, less func(i, j int) bool) {
    if less == nil {
        less = func(i, j int) bool { return s[i] < s[j] }
    }
    sort.Slice(s, less)
}

func main() {
    food := []string{"onion", "carrot", "celery"}
    sortStrings(food, nil)
    fmt.Println(food)             1
}

  • 1 Prints [carrot celery onion]
Quick check 27.3

Q1:

Write a line of code to sort food from the shortest to longest string in listing 27.6.

QC 27.3 answer

1:

sortStrings(food, func(i, j int) bool { return len(food[i]) < len(food[j]) })

 

27.4. Nil slices

A slice that’s declared without a composite literal or the make built-in will have a value of nil. Fortunately, the range keyword, len built-in, and append built-in all work with nil slices, as shown in the following listing.

Listing 27.7. Growing a slice: slice.go
var soup []string
fmt.Println(soup == nil)                             1

for _, ingredient := range soup {
    fmt.Println(ingredient)
}

fmt.Println(len(soup))                               2

soup = append(soup, "onion", "carrot", "celery")
fmt.Println(soup)                                    3

  • 1 Prints true
  • 2 Prints 0
  • 3 Prints [onion carrot celery]

An empty slice and a nil slice aren’t equivalent, but they can often be used interchangeably. The following listing passes nil to a function that accepts a slice, skipping the step of making an empty slice.

Listing 27.8. Start with nil: mirepoix.go
func main() {
    soup := mirepoix(nil)
    fmt.Println(soup)          1
}

func mirepoix(ingredients []string) []string {
    return append(ingredients, "onion", "carrot", "celery")
}

  • 1 Prints [onion carrot celery]

Whenever you write a function that accepts a slice, ensure that a nil slice has the same behavior as an empty slice.

Quick check 27.4

Q1:

Which actions are safe to perform on a nil slice?

QC 27.4 answer

1:

The built-ins len, cap, and append are safe to use with a nil slice, as is the range key-word. As with an empty slice, directly accessing an element of a nil slice (soup[0]) will panic with index out of range.

 

27.5. Nil maps

As with slices, a map declared without a composite literal or the make built-in has a value of nil. Maps can be read even when nil, as shown in the following listing, though writing to a nil map will panic.

Listing 27.9. Reading a map: map.go
var soup map[string]int
fmt.Println(soup == nil)            1

measurement, ok := soup["onion"]
if ok {
    fmt.Println(measurement)
}

for ingredient, measurement := range soup {
    fmt.Println(ingredient, measurement)
}

  • 1 Prints true

If a function only reads from a map, it’s fine to pass the function nil instead of making an empty map.

Quick check 27.5

Q1:

What action on a nil map will cause a panic?

QC 27.5 answer

1:

Writing to a nil map (soup["onion"] = 1) will panic with: assignment to entry in nil map.

 

27.6. Nil interfaces

When a variable is declared to be an interface type without an assignment, the zero value is nil. The following listing demonstrates that the interface type and value are both nil, and the variable compares as equal to nil.

Listing 27.10. Interfaces can be nil: interface.go
var v interface{}
fmt.Printf("%T %v %v
", v, v, v == nil)       1

  • 1 Prints <nil> <nil> true

When a variable with an interface type is assigned a value, the interface internally points to the type and value of that variable. This leads to the rather surprising behavior of a nil value that doesn’t compare as equal to nil. Both the interface type and value need to be nil for the variable to equal nil, as shown in the following listing.

Listing 27.11. Wat?: interface.go
var p *int
v = p
fmt.Printf("%T %v %v
", v, v, v == nil)      1

  • 1 Prints *int <nil> false

The %#v format verb is shorthand to see both type and value, also revealing that the variable contains (*int)(nil) rather than just <nil>, as shown in listing 27.12.

Listing 27.12. Inspecting the Go representation: interface.go
fmt.Printf("%#v
", v)          1

  • 1 Prints (*int)(nil)

To avoid surprises when comparing interfaces to nil, it’s best to use the nil identifier explicitly, rather than pointing to a variable that contains a nil.

Quick check 27.6

Q1:

What’s the value of s when declared as var s fmt.Stringer?

QC 27.6 answer

1:

The value is nil because fmt.Stringer is an interface and the zero value for interfaces is nil.

 

27.7. An alternative to nil

It can be tempting to adopt nil whenever a value can be nothing. For example, a pointer to an integer (*int) can represent both zero and nil. Pointers are intended for pointing, so using a pointer just to provide a nil value isn’t necessarily the best option.

Instead of using a pointer, one alternative is to declare a small structure with a few methods. It requires a little more code, but it doesn’t require a pointer or nil, as shown in the following listing.

Listing 27.13. Number is set: valid.go
type number struct {
    value int
    valid bool
}

func newNumber(v int) number {
    return number{value: v, valid: true}
}

func (n number) String() string {
    if !n.valid {
        return "not set"
    }
    return fmt.Sprintf("%d", n.value)
}

func main() {
    n := newNumber(42)
    fmt.Println(n)          1

    e := number{}
    fmt.Println(e)          2
}

  • 1 Prints 42
  • 2 Prints not set
Quick check 27.7

Q1:

What are some advantages to the approach taken in listing 27.13?

QC 27.7 answer

1:

It completely avoids nil pointer dereferences by not having pointers or nil values. The valid Boolean has a clear intention, whereas the meaning of nil is less clear.

 

Summary

  • Nil pointer dereferences will crash your program.
  • Methods can guard against receiving nil values.
  • Default behavior can be provided for functions passed as arguments.
  • A nil slice is often interchangeable with an empty slice.
  • A nil map can be read from but not written to.
  • If an interface looks like it’s nil, be sure both the type and value are nil.
  • Nil isn’t the only way to represent nothing.

Let’s see if you got this...

Experiment: knights.go

A knight blocks Arthur’s path. Our hero is empty-handed, represented by a nil value for leftHand *item. Implement a character struct with methods such as pickup(i *item) and give(to *character). Then use what you’ve learned in this lesson to write a script that has Arthur pick up an item and give it to the knight, displaying an appropriate description for each action.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset