LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2417 min read

Pointers

Learn how Go pointers work using & and *, why they let you mutate values and avoid unnecessary copies, and why Go has no pointer arithmetic.

Introduction

By default, Go passes arguments to functions by value — meaning a copy is made. For small values like an int or a bool, that's cheap and usually exactly what you want. But sometimes you need a function to modify the caller's original data, or you want to avoid the cost of copying a large struct on every call. Pointers solve both problems by letting you work with a value's memory address directly.

What You Will Learn
  • What a pointer is and how it relates to memory addresses.
  • How to use the & and * operators.
  • Why pointers let you mutate values without copying them.
  • How pointers behave as function parameters.
  • Why Go deliberately omits pointer arithmetic, unlike C.
  • What a nil pointer is and how to guard against it.

What Is a Pointer?

A pointer is a variable that stores the memory address of another variable, rather than a value itself. A pointer to a value of type T has the type *T. Pointers let you refer to "the same variable" from multiple places, instead of working with separate copies.

package main
import "fmt"
func main() {
age := 30
var agePointer *int = &age
fmt.Println("value of age:", age)
fmt.Println("address of age:", agePointer)
fmt.Println("value via pointer:", *agePointer)
}
Output

Click Run to see what this code prints.

The exact address printed (0xc0000140a0) will differ every time you run the program — memory addresses are assigned dynamically. What matters is the pattern: & gets an address, and * follows it back to the value.

The & and * Operators

The & (address-of) operator produces a pointer to a variable. The * (dereference) operator, when applied to a pointer, gives you back the value it points to — and can also be used to modify that original value.

package main
import "fmt"
func main() {
score := 100
scorePointer := &score
fmt.Println("before:", score)
*scorePointer = 150 // modifies the original "score" through the pointer
fmt.Println("after:", score)
}
Output

Click Run to see what this code prints.

Why Use Pointers: Mutating Without Copying

Passing a value to a function normally passes a copy — changes inside the function never affect the caller's variable. Passing a pointer instead lets the function reach back and modify the original.

package main
import "fmt"
// By value: has no effect on the caller's variable.
func incrementByValue(n int) {
n++
}
// By pointer: modifies the caller's original variable.
func incrementByPointer(n *int) {
*n++
}
func main() {
count := 10
incrementByValue(count)
fmt.Println("after incrementByValue:", count) // unchanged
incrementByPointer(&count)
fmt.Println("after incrementByPointer:", count) // changed
}
Output

Click Run to see what this code prints.

Pointers and Function Parameters

Pointers are also useful purely for efficiency: passing a pointer to a large struct copies only the (small, fixed-size) address, rather than copying the entire struct's data on every function call.

package main
import "fmt"
type Profile struct {
Name string
Email string
Bio string // imagine this is a very large field
}
// Passing a pointer avoids copying the whole Profile struct.
func updateBio(p *Profile, newBio string) {
p.Bio = newBio // Go automatically dereferences for field access
}
func main() {
user := Profile{Name: "Dana", Email: "dana@example.com", Bio: "old bio"}
updateBio(&user, "Go developer and open-source contributor")
fmt.Println(user.Bio)
}
Output

Click Run to see what this code prints.

Automatic Dereferencing

Notice p.Bio, not (*p).Bio. Go automatically dereferences a pointer to a struct when you access a field, so you rarely need to write the explicit (*p).Field form.

No Pointer Arithmetic

If you've used C or C++, you may be used to pointer arithmetic — adding an offset to a pointer to walk through memory, e.g. ptr + 1 to move to the next element. Go deliberately does not allow this. You cannot add, subtract, or compare pointers with arithmetic operators. This is a safety decision: pointer arithmetic is a major source of memory corruption bugs and security vulnerabilities in C-style languages, and Go's designers chose to eliminate that entire category of bugs.

package main
func main() {
x := 5
p := &x
// The following line would NOT compile in Go:
// p = p + 1 // invalid operation: p + 1 (mismatched types *int and untyped int)
_ = p
}
Attempting pointer arithmetic

Click Run to see what this code prints.

If you need to walk through a sequence of values, use a slice and indexing instead — slices give you the safety of bounds-checked access without ever touching a raw memory address.

The Zero Value: nil

A pointer's zero value is nil, meaning it points to nothing. Dereferencing a nil pointer causes a runtime panic, so it's good practice to check for nil before dereferencing when a pointer might not have been initialized.

package main
import "fmt"
func safePrint(p *int) {
if p == nil {
fmt.Println("pointer is nil, nothing to print")
return
}
fmt.Println("value:", *p)
}
func main() {
var p *int // nil by default
safePrint(p)
x := 42
safePrint(&x)
}
Output

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Dereferencing a nil pointer without checking first, causing a runtime panic.
  • Overusing pointers for small, cheap-to-copy values like int or bool, where a value type is simpler and just as fast.
  • Confusing & (get the address of a value) with * (follow a pointer to its value) — they are opposites.
  • Trying to perform pointer arithmetic, expecting C-like behavior — Go simply does not support it.
  • Returning a pointer to a local variable and worrying it becomes invalid — Go's escape analysis safely moves it to the heap automatically, unlike C.

Best Practices

  • Use pointers when a function needs to mutate the caller's data, or when passing a large struct where copying would be wasteful.
  • Prefer plain values for small, simple types unless mutation or performance specifically calls for a pointer.
  • Check for nil before dereferencing a pointer that might not have been initialized.
  • Let Go's escape analysis handle memory placement — don't worry about "returning a pointer to a local variable" the way you would in C.
  • Be consistent within a type's method set: if some methods use pointer receivers, prefer using pointer receivers throughout for that type.

Frequently Asked Questions

Yes. Unlike C, Go's compiler performs escape analysis and automatically allocates the variable on the heap if it needs to outlive the function, so returning &localVar is completely safe.

No. Go is garbage collected — memory is automatically reclaimed once nothing references it anymore. There is no manual free() as in C.

Yes, pointer equality compares whether both pointers point to the same memory address, not whether the pointed-to values are equal.

It removes an entire class of bugs (buffer overruns, invalid memory access) that plague pointer arithmetic in C-style languages, in exchange for a small amount of low-level flexibility that slices and the unsafe package can cover when truly needed.

Key Takeaways

  • A pointer stores a memory address; *T is the type of a pointer to a T.
  • & takes the address of a variable; * dereferences a pointer to access or modify its value.
  • Pointers let functions mutate the caller's data and avoid copying large values.
  • Go has no pointer arithmetic — this eliminates a major class of memory-safety bugs found in C.
  • A nil pointer points to nothing; dereferencing one panics, so check before you dereference.

Summary

Pointers give Go the ability to mutate shared state and avoid unnecessary copying, while Go's garbage collector and lack of pointer arithmetic keep them far safer than their C counterparts. With pointers under your belt, you're ready to learn about structs — the primary way Go groups related data together.

Next Lesson →

Structs