July 7, 2026

Go Pointers – Addresses, Dereferencing, and When to Use Them

Updated July 2, 2026. Refreshed for Go 1.22+ and current SEO best practices.

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 nil represents “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.


0 0 votes
Article Rating
Subscribe
Notify of
1 Comment
Oldest
Newest Most Voted
trackback

[…] Previous: Lesson 9: Go Pointers – Addresses, Dereferencing, and When to Use Them […]

1
0
Would love your thoughts, please comment.x
()
x