What Are Pointers in Go and Why Use Them

Go pointers store memory addresses to modify variables directly and avoid expensive data copying.

The photocopy problem

You write a function to update a user profile. You pass the user struct. You change the email inside the function. You print the user back in main. The email didn't change. You stare at the screen. In JavaScript, objects are references. In Python, names point to objects. Go does something else. Go copies everything by default. Your function got a photocopy of the user. You edited the photocopy. The original user sat untouched in main. Pointers fix this. They let you hand over the address of the original data instead of a copy.

Pointers are addresses

A pointer is a variable that holds a memory address. Think of a variable as a box containing a value. A pointer is a box containing the location of another box. If you want to change what's inside the second box, you follow the location in the pointer box, find the target box, and swap the contents.

In Go, the & operator gives you the address of a variable. The * operator follows the address to get or set the value. This is called dereferencing. The compiler tracks pointer types strictly. A pointer to an integer is *int. A pointer to a string is *string. You cannot assign a *string to a variable of type *int. The compiler rejects the mismatch immediately.

Here's the mechanics in isolation. You create a value, take its address, and mutate it through the pointer.

package main

import "fmt"

func main() {
	// val holds an integer on the stack.
	val := 10

	// ptr holds the memory address of val.
	// The & operator takes the address.
	ptr := &val

	// *ptr accesses the value at that address.
	// This modifies val directly, not a copy.
	*ptr = 20

	fmt.Println(val)
}

When the compiler sees val := 10, it allocates space for an integer. When it sees &val, it calculates the address of that space. ptr stores that address. When you write *ptr = 20, the runtime looks up the address in ptr, finds the integer slot, and writes 20 there. val and *ptr refer to the same slot. Changing one changes the other.

Pointers are addresses. Nil is a trap.

Go passes everything by value

This is the part that trips up developers coming from reference-heavy languages. Go passes arguments by value. Always. Even pointers are passed by value.

When you pass a pointer to a function, the address is copied. The function receives a copy of the pointer. Both the caller and the callee hold a copy of the address. Both copies point to the same underlying data. Mutation works because the copies point to the same place, not because the pointer itself is shared by reference.

Here's a demonstration. The function receives a copy of the pointer, but dereferencing that copy changes the shared value.

package main

import "fmt"

func main() {
	// val is the original integer.
	val := 10

	// ptr holds the address of val.
	ptr := &val

	// Pass ptr to modify.
	// Go copies the pointer value (the address).
	// modify receives a local copy of the address.
	modify(ptr)

	// val changed because both pointers targeted the same slot.
	fmt.Println(val)
}

// modify receives a copy of the pointer.
// Dereferencing the copy updates the shared value.
func modify(p *int) {
	*p = 99
}

The variable p inside modify is a local variable. It holds a copy of the address. If you reassign p inside the function, you only change the local copy. The caller's pointer remains unchanged. Only dereferencing p affects the shared data.

Pass by value, always. Even pointers are values.

Realistic usage: structs and methods

Real code usually involves structs. You use pointers to mutate state or avoid copying large data. Copying a small integer is cheap. Copying a struct with twenty fields and embedded slices is expensive. Passing a pointer avoids the copy. The function receives a small address instead of a large block of memory.

When you define methods on structs, you choose between a value receiver and a pointer receiver. The receiver naming convention matters. Use one or two letters matching the type. Write (u *User), not (this *User) or (self *User). The community expects short receiver names.

Here's a service function that updates a user. It takes a pointer receiver to modify the struct in place.

package main

import "fmt"

type User struct {
	ID    int
	Email string
}

// UpdateEmail modifies the user in place.
// It takes a pointer to avoid copying the struct.
func (u *User) UpdateEmail(newEmail string) {
	u.Email = newEmail
}

func main() {
	// user is a struct value.
	user := User{ID: 1, Email: "old@example.com"}

	// Call the method on the address of user.
	// Go automatically takes the address if the receiver is a pointer.
	user.UpdateEmail("new@example.com")

	fmt.Println(user.Email)
}

The call user.UpdateEmail(...) works even though the receiver is *User. Go is smart enough to take the address of user automatically when you call a pointer method on an addressable value. This is a convenience feature. The compiler inserts the & for you. You can also write (&user).UpdateEmail(...) explicitly, but the shorthand is standard.

Mutate in place. Save copies.

Pitfalls and compiler errors

The biggest risk is the nil pointer. A pointer can be nil, meaning it points nowhere. If you dereference a nil pointer, the program panics. The runtime crashes with invalid memory address or nil pointer dereference. You must check for nil before dereferencing, or ensure the pointer is always initialized.

func safePrint(p *string) {
	// Check for nil before dereferencing.
	// Dereferencing a nil pointer causes a runtime panic.
	if p == nil {
		fmt.Println("no value")
		return
	}
	fmt.Println(*p)
}

The compiler helps with some mistakes. If you try to take the address of a constant or a method call, the compiler rejects it with cannot take the address of.... You can only take the address of addressable values, like variables or struct fields.

func main() {
	// This fails. Constants are not addressable.
	// The compiler rejects this with cannot take the address of constant.
	// ptr := &10

	// This works. Variables are addressable.
	val := 10
	ptr := &val
	_ = ptr
}

Avoid passing pointers to strings. Strings are already cheap to pass by value. A string in Go is a struct containing a pointer to a byte array and a length. Passing a string copies that small struct. Passing a *string adds an extra level of indirection without saving memory. It also introduces the risk of nil pointers for no benefit. Pass the string by value.

Don't fight the type system. Wrap the value or change the design.

When to use pointers

Go encourages simplicity. You don't need pointers for everything. Use them when they solve a specific problem. Use values when they are safer and faster.

Use a pointer when you need to modify the original value inside a function.

Use a pointer when the value is large and copying it is expensive, such as a struct with many fields or embedded slices.

Use a pointer when you need to represent the absence of a value, since a pointer can be nil.

Use a pointer when building linked data structures like trees or lists where nodes reference each other.

Use a value when the data is small and immutable, like an integer, boolean, or short string.

Use a value when you want to guarantee the caller cannot modify the data through the function.

Use a value when performance matters and the cost of copying is lower than the cost of indirection.

Trust gofmt. Argue logic, not formatting.

Where to go next