In this lesson we cover the Go pointers tutorial essentials — what pointers are, how to take addresses and dereference values, when to pass pointers to functions, and how pointers interact with structs and methods. If you completed Go Structs and Methods, you already met pointer receivers on methods. Here we go deeper so you understand why Go uses pointers and how to avoid common mistakes.
Prerequisites: Lessons 1–8 completed, Go 1.22 or newer installed. Estimated time: 40–50 minutes.
1. What Is a Pointer?
A pointer is a variable that stores the memory address of another value — not the value itself. In Go, you declare a pointer type by placing * before the underlying type. The zero value of a pointer is nil, meaning it points to nothing.
Two operators work with pointers:
&(address-of) — returns the memory address of a variable*(dereference) — reads or writes the value at that address
package main
import "fmt"
func main() {
x := 42
p := &x // p is *int — address of x
fmt.Println("value of x:", x) // 42
fmt.Println("address stored in p:", p)
fmt.Println("value through p:", *p) // dereference — 42
*p = 100 // change x through the pointer
fmt.Println("x after *p = 100:", x) // 100
}
Unlike C, Go has no pointer arithmetic — you cannot add or subtract from a pointer to walk through memory. This design choice keeps Go memory-safe while still giving you direct control when you need it.
2. Pointers vs Values in Function Calls
Go passes arguments by value by default. When you pass an integer or a struct to a function, the function receives a copy. Changes inside the function do not affect the original unless you pass a pointer.
package main
import "fmt"
func incrementValue(n int) {
n++ // only changes the local copy
}
func incrementPointer(n *int) {
*n++ // changes the original variable
}
func main() {
count := 10
incrementValue(count)
fmt.Println("after incrementValue:", count) // 10 — unchanged
incrementPointer(&count)
fmt.Println("after incrementPointer:", count) // 11 — changed
}
Use pointers when a function must modify the caller’s data, when copying a large struct would be expensive, or when you need to represent optional or shared state. For small values like integers and booleans, passing by value is often clearer and just as fast.
3. Creating Pointers with new and Struct Literals
The built-in new(T) allocates memory for type T, zeroes it, and returns *T. For structs, the more idiomatic approach is a struct literal with &:
package main
import "fmt"
type Book struct {
Title string
Author string
Pages int
}
func main() {
// new() — returns pointer to zero-valued Book
b1 := new(Book)
b1.Title = "The Go Programming Language"
fmt.Println(b1.Title)
// struct literal with & — preferred style
b2 := &Book{
Title: "Concurrency in Go",
Author: "Katherine Cox-Buday",
Pages: 250,
}
fmt.Printf("%+v\n", b2)
}
Both approaches produce a *Book. The struct literal form is more readable because you can set fields at creation time. Constructor functions like NewBook(...) that return *Book are common in production code — you saw this pattern with NewAccount in the Structs lesson.
4. Nil Pointers and Safety
Dereferencing a nil pointer causes a runtime panic. Always check whether a pointer is nil before dereferencing when the pointer might not be initialized:
package main
import "fmt"
type User struct {
Name string
}
func greet(u *User) {
if u == nil {
fmt.Println("no user provided")
return
}
fmt.Println("Hello,", u.Name)
}
func main() {
var u *User // nil pointer — zero value for *User
greet(u) // safe — handled
u = &User{Name: "Ada"}
greet(u) // Hello, Ada
}
Go’s type system does not prevent nil pointer dereferences at compile time. Defensive nil checks at API boundaries — especially in functions that accept pointers from callers — prevent crashes in production. Methods with pointer receivers can also be called on nil receivers in some cases (when the method does not dereference fields), but relying on that is rare.
5. Pointer Receivers on Methods
When a method uses a pointer receiver (t *Type), it can modify the struct and avoids copying large values on each call. This is the pattern you need for mutating methods and for consistency when any method on a type modifies state:
package main
import "fmt"
type Counter struct {
count int
}
// Value receiver — gets a copy; cannot change original
func (c Counter) ValueReceiver() int {
return c.count
}
// Pointer receiver — can mutate the struct
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Current() int {
return c.count
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.Current()) // 2
}
Go automatically takes the address when you call a pointer-receiver method on a value (c.Increment() works even though c is a value). The compiler inserts (&c).Increment() behind the scenes. Choose pointer receivers when the method mutates the receiver or when the struct is large; use value receivers for small, immutable types.
6. Pointers, Slices, and Maps
Not everything needs an explicit pointer. Slices and maps are reference types — they internally hold a pointer to underlying data. Passing a slice to a function lets you modify elements without *:
package main
import "fmt"
func addTag(tags []string, tag string) {
tags = append(tags, tag) // may not change caller's slice header
}
func addTagPointer(tags *[]string, tag string) {
*tags = append(*tags, tag) // changes caller's slice
}
func main() {
tags := []string{"go", "tutorial"}
addTag(tags, "pointers")
fmt.Println(tags) // [go tutorial] — unchanged
addTagPointer(&tags, "pointers")
fmt.Println(tags) // [go tutorial pointers]
}
Understanding this distinction prevents over-using pointers. Modify slice elements through the slice directly; use a pointer to the slice only when you need to reassign the slice header (length, capacity, or backing array). Maps behave similarly — you pass the map value, but it references shared underlying data.
7. When to Use Pointers — Practical Guidelines
Follow these guidelines when deciding between values and pointers in your Go code:
- Use pointers when methods mutate struct state, when structs are large (dozens of fields or big arrays), or when
nilrepresents “not present” (optional fields). - Use values for small, immutable data (coordinates, IDs, enums), for concurrency safety (copies avoid shared mutation), and when zero values are meaningful defaults.
- Avoid pointers to primitives in public APIs unless mutation is required — they add cognitive overhead for little gain.
These rules align with the standard library. Types like time.Time are structs passed by value because they are small and immutable. Types like bytes.Buffer use pointer receivers because they accumulate mutable state.
8. Next Steps
You now understand Go pointers — addresses, dereferencing, nil safety, pointer receivers, and when to pass pointers versus values. In the next lesson we cover interfaces and error handling — Go’s approach to polymorphism and the idiomatic way to signal and handle failures.
Continue the series: Lesson 8 – Structs and Methods · Lesson 10 – Interfaces and Errors
Frequently Asked Questions
What is a pointer in Go?
A pointer holds the memory address of a value. Use & to get an address and * to read or write through it. The zero value is nil.
When should I use pointer receivers?
Use pointer receivers when methods modify the struct, when the struct is large, or when you want consistency across all methods on a type. Value receivers work for small, immutable types.
Can Go pointers cause memory leaks?
Go has garbage collection, so you do not manually free memory. However, holding pointers to large structures longer than needed can delay collection — design APIs to release references when done.
Want live Go or backend classes? Join Alkademy for instructor-led programming courses with hands-on projects.
[…] Previous: Lesson 9: Go Pointers – Addresses, Dereferencing, and When to Use Them […]