HowtoGo
Home / Data Structures / Pointers
Data Structures

Pointers

A pointer holds the memory address of a value, not the value itself. Passing a pointer shares and modifies the original data without copying it.

The *T syntax denotes a pointer to a value of type T. Here, p is a pointer to an integer. Its default zero-value is nil.

var p *int

The & operator generates a pointer to its operand. It takes the memory address of the variable i and assigns it to p.

i := 42
p = &i

To read the underlying value that the pointer is looking at, use the * operator. This is called "dereferencing" the pointer.

fmt.Println(*p)

You can also use the * operator to change the underlying value. Modifying *p directly alters the original variable i.

*p = 21

21 confirms *p = 21 changed i itself. p and i share the same memory address throughout.

Terminal
$ go run pointers.go
42
21
The idea

A pointer is a variable holding an address. p does not contain 7; it contains the place 7 lives.

Point it somewhere else and the arrow moves. Write through it and you change the value at the other end, not a copy.

A pointer holding no address is nil, and reading through it panics.

Interactive diagram of a pointer, the address it holds, and the value at that address
the pointer what it points at

Point p at either variable, write through it, or set it to nil.

What pointers are actually for

A function gets copies of its arguments, so it cannot change what the caller passed. Hand it the address instead and it can reach back through the pointer to the original.

This is the whole reason pointers exist in most Go code. Everything else is a variation on it.

func double(n *int) {
    *n *= 2
}

x := 10
double(&x)
fmt.Println(x)

The same rule decides whether a method can change its receiver. Inc takes *Counter and mutates the real one; IncCopy takes Counter, increments a copy, and drops it on return.

Go calls c.Inc() on an addressable value by taking its address for you, so the call site looks identical either way. Nothing warns you that one of them did nothing.

type Counter struct{ n int }

func (c *Counter) Inc()     { c.n++ }  // changes the original
func (c Counter) IncCopy()  { c.n++ }  // changes a copy, then throws it away

c := Counter{}
c.Inc()
c.Inc()
c.IncCopy()
fmt.Println(c.n)

Things Go does differently

Returning a pointer to a local variable is safe here, which surprises anyone arriving from C. The compiler notices the value outlives the function and puts it on the heap instead of the stack.

There is no dangling pointer to create, so a constructor handing back a pointer to a value it just made is ordinary Go rather than a bug waiting to happen.

func newCounter(start int) *Counter {
    c := Counter{n: start}
    return &c
}

p := newCounter(5)
p.Inc()
fmt.Println(p.n)

A method with a pointer receiver can be called on a nil pointer. The receiver is simply nil inside, and a method that checks for it works.

That is what lets the recursive Sum stop: the last node's Next is nil, and calling Sum on it returns 0 rather than panicking. Reading a field through a nil pointer still panics; only the method call is safe.

type Node struct {
    Val  int
    Next *Node
}

func (n *Node) Sum() int {
    if n == nil {
        return 0
    }
    return n.Val + n.Next.Sum()
}

list := &Node{1, &Node{2, &Node{3, nil}}}
fmt.Println(list.Sum())

var empty *Node
fmt.Println(empty.Sum())

Go gives you pointers for reaching a caller's value and for saying "nothing here", and takes away the parts that make them dangerous: no arithmetic on them, and no way to end up pointing at memory that has gone.

Terminal
$ go run pointers.go
20
2
6
6
0