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 *
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) // trueJava references vs Go values and pointers
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 10m1 := 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 5Java: m2 = m1; m2.retries = 5
Go: m2 := m1; m2.Retries = 5
Go pointers: m4 := m3; m4.Retries = 5
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
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
// 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
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()
}