Skip to content
Theory · Pointers · lesson 5 of 8

The nil pointer and new

What happens if you take a slip for an address but write nothing on it? There is nowhere to go. In Go that is a pointer with no address. A pointer declared with var does not point anywhere yet. A program that writes through it compiles, but stops with a panic when it runs:

GO
package main

func main() {
    var p *int
    *p = 5 // ✗
}
TEXT
panic: runtime error: invalid memory address or nil pointer dereference

The first line of the panic names the cause: an invalid memory address while dereferencing a nil pointer. Why didn't the compiler catch it?

  • It checks types, and *p has the right type, int.
  • That the slip is blank only turns out while the program runs.

This panic is one of the most common ones in real Go programs.

The nil value

The zero value of any pointer is nil—that blank slip. You can print it and compare it with nil:

GO
var p *int
fmt.Println(p, p == nil) // <nil> true

What you can and cannot do with a nil pointer:

  • you can print it and compare it with nil, as in the example above;
  • you cannot dereference it: reading *p causes the same panic as writing;
  • you cannot read a 0 from there either: there is no house behind nil.

This is where a pointer differs from a map: a missing map key reads as the zero value, but a nil pointer does not.

A check before dereferencing

If a pointer may be nil, check it before dereferencing: if p != nil { fmt.Println(*p) }. A function with a pointer parameter needs this check too: the caller may have handed it a blank slip.

Two ways to get an address

For a pointer not to be nil, it needs an address. There are two ways to get one.

The address of an existing variable

The & operator takes the address of a variable that already exists: p := &x. The slip leads to the house named x, so you can reach it both by name and by the slip. A write *p = 5 shows through x.

The new function

You can ask Go to build a new house. The built-in function new does that: it creates a new variable of the given type and returns its address. The variable gets the zero value of its type:

GO
p := new(int)
fmt.Println(*p) // 0
*p = 5
fmt.Println(*p, p != nil) // 5 true

Let's see what happened here:

  1. new(int) built a house, put 0 in it and handed back a slip with its address.
  2. So the first Println prints 0.
  3. *p = 5 puts 5 into the house, and the second Println prints 5 true.

p := new(int) works like the pair of lines x := 0 and p := &x. There is one difference: the new house has no name of its own, and you can reach it only through the slip p. So new is handy when you need a pointer but no separate name for the variable.

Each call to new builds a separate house. After a := new(int) and b := new(int), the comparison a == b is false, while *a == *b is true. The variables have different addresses but equal values.

new and make

Interviewers like to ask how new differs from make. The answer has two parts:

  • make creates slices and maps and returns a ready value, not a pointer;
  • new works for any type and always returns a pointer.

Common mistake

A pointer was checked for nil through a dereference:

GO
p := new(int)
if *p == nil { // ✗
    fmt.Println("no value")
}
TEXT
invalid operation: *p == nil (mismatched types int and untyped nil)

The program does not compile: *p is a number of type int, and a number is never nil. A slip can be blank, a house cannot, so you compare the pointer itself with nil: p == nil.

Key points

  • The zero value of a pointer is nil: a pointer with no address.
  • Reading or writing through a nil pointer compiles, but panics when the program runs.
  • new(T) creates a variable of type T with its zero value and returns its address; make creates slices and maps.
  • Compare the pointer with nil (p == nil), not *p.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way