Skip to content
JavaGo
Part II · Types & Data

Pointers

Java hides references; Go makes you choose between a value and a pointer. When to use each, and why it is safer than C.

Section 07 of 31 6 min read

A pointer holds the address of a value rather than the value itself. Go makes the choice explicit: every variable, field and parameter is either a value (Message) or a pointer to one (*Message). Unlike C, there is no pointer arithmetic and memory is garbage collected, so pointers in Go are about sharing and mutation, not manual memory management.

The two operators: & and *

& takes an address, * follows one
x := 42
p := &x // & = "address of x". p has type *int

*p = 100 // * = "the value p points to". Writes to x.

fmt.Println(x)  // 100: x changed through p
fmt.Println(*p) // 100: reading through the pointer
fmt.Println(p)  // 0xc000012080: the address itself

var q *int      // zero value of a pointer type is nil
fmt.Println(q == nil) // true

Java references vs Go values and pointers

Java — objects are always shared
Message m1 = new Message();
Message m2 = m1;   // same object
m2.retries = 5;    // m1.retries is 5 too

int a = 10;
int b = a;         // primitives copy
b = 20;            // a is still 10
Go — you choose: copy or share
m1 := Message{Retries: 0}
m2 := m1           // a full copy of the struct
m2.Retries = 5     // m1.Retries is still 0

m3 := &Message{Retries: 0}
m4 := m3           // copies the pointer: same Message
m4.Retries = 5     // m3.Retries is now 5

Java: m2 = m1; m2.retries = 5

Go: m2 := m1; m2.Retries = 5

Go pointers: m4 := m3; m4.Retries = 5

Fig. 7.1Java variables of object type hold references, so m2 = m1 shares one object. A Go struct variable holds the struct itself, so m2 := m1 copies every field. Sharing in Go takes a pointer, written explicitly with &.

Automatic dereferencing

no -> operator: . works through pointers
msg := &Message{Topic: "orders"} // msg is *Message

(*msg).Topic = "updated" // explicit dereference: legal but never written
msg.Topic = "updated"    // Go inserts (*msg) for you
msg.Retries++

// Method calls adapt in both directions:
msg.IncrementRetry() // pointer receiver on a pointer: fine
v := Message{}
v.IncrementRetry()   // pointer receiver on an addressable value:
                     // Go calls (&v).IncrementRetry()

When to use a pointer

four reasons you will see *T
// 1. MUTATION: the method must change the caller's struct
func (m *Message) IncrementRetry() { m.Retries++ }

// 2. LARGE VALUES: avoid copying big structs on every call.
//    Note: slices, maps, strings and channels are small headers.
//    Copying a struct with a 1 MB []byte copies 24 bytes, not 1 MB.
type Frame struct {
    Pixels [1920 * 1080]uint32 // an array IS the data: ~8 MB
}
func Render(f *Frame) { ... }   // pass the address, not 8 MB

// 3. SHARED IDENTITY: one pool, cache or server used by everyone
func NewServer(store MessageStore) *Server {
    return &Server{store: store}
}

// 4. OPTIONAL VALUES: nil means "not set"
type Config struct {
    MaxRetries *int           // nil: use the default
    Timeout    *time.Duration // nil: no timeout
}
cfg := Config{MaxRetries: new(5)} // Go 1.26+: new(expr)

Nil pointers

nil pointer dereference: the Go NullPointerException
var msg *Message // declared but never assigned: nil

// msg.Topic
// panic: runtime error: invalid memory address or nil pointer dereference

// Check pointers you did not create yourself
func (s *Server) Dispatch(msg *Message) error {
    if msg == nil {
        return errors.New("dispatch: nil message")
    }
    return s.store.Save(*msg)
}

// Methods can be called on a nil pointer receiver; this is legal
// and occasionally useful, e.g. a nil *Tree representing "empty".
func (t *Tree) Len() int {
    if t == nil {
        return 0
    }
    return 1 + t.Left.Len() + t.Right.Len()
}
↑ ↓ to navigateEnter to openEsc to close