Pointers

Reviewed & published by Brayan K

Pointers let a function reach back and change the caller's data instead of working on a copy. Go's pointers are far gentler than C's — no pointer arithmetic, garbage-collected memory — but the two operators & (address-of) and * (dereference) unlock efficient, mutable code. This lesson demystifies them once and for all.

Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Address-of & Dereference

Two operators do all the work. &x gives you the address of x — a value of type *int if x is an int. *p dereferences the pointer: it follows the address to read or write the value living there. Writing through a pointer changes the original variable — that's the whole point.

package main

import "fmt"

func main() {
    // A pointer holds the ADDRESS of a value, not the value itself.

    x := 10
    p := &x          // & = "address of x". p has type *int.
    fmt.Println(p == nil)  // false — p points at real memory

    // * = "dereference": follow the pointer to the value it points at.
    fmt.Println(*p)  // 10  (the value at that address)

    // Writing through the pointer changes the ORIGINAL variable.
    *p = 25
    fmt.Println(x)   // 25  (x changed, via p)

    // The pointer itself is just an address; printing it shows hex.
    fmt.Printf("%T\n", p) // *int
}

2️⃣ Pointers as Function Parameters

This is where pointers earn their keep. Go passes everything by value — a function receives a copy, so editing a plain parameter never touches the caller's variable. Pass a pointer instead and the function can modify the original by dereferencing it. Compare the two functions below: only the pointer version's change survives.

package main

import "fmt"

func main() {
    balance := 100

    // Passing a VALUE copies it — the function can't change the original.
    addByValue(balance, 50)
    fmt.Println("after value:", balance) // after value: 100  (unchanged)

    // Passing a POINTER lets the function modify the caller's variable.
    addByPointer(&balance, 50)
    fmt.Println("after pointer:", balance) // after pointer: 150
}

func addByValue(b int, amount int) {
    b += amount  // edits the local copy only
}

func addByPointer(b *int, amount int) {
    *b += amount // edits the value the pointer points at
}

3️⃣ new, nil & Structs

The built-in new(T) allocates a zeroed T and returns a *T. Every pointer's zero value is nil, and dereferencing a nil pointer panics — so guard with a check when a pointer might be unset. A lovely convenience: with a pointer to a struct, Go lets you write p.Field and auto-dereferences for you — no need for the clunky (*p).Field.

package main

import "fmt"

type Counter struct {
    Hits int
}

func main() {
    // new(T) allocates a zeroed T and returns a *T pointer to it.
    p := new(int)        // p is *int, *p is 0
    *p = 7
    fmt.Println(*p)      // 7

    // The zero value of any pointer is nil. Dereferencing nil PANICS,
    // so guard with a check when a pointer might be unset.
    var q *int
    fmt.Println(q == nil) // true

    // With structs, Go auto-dereferences field access through a pointer:
    // you write cp.Hits, not (*cp).Hits.
    c := Counter{}
    cp := &c
    cp.Hits++            // same as (*cp).Hits++
    cp.Hits++
    fmt.Println(c.Hits)  // 2  (original struct changed)
}

Your turn — take an address, then write through the pointer.

package main

import "fmt"

func main() {
    // 🎯 YOUR TURN — replace each ___ then run it.
    score := 5

    // 1) Take the address of score
    p := ___score          // 👉 &score

    // 2) Change score to 9 by writing THROUGH the pointer
    ___ = 9                // 👉 *p

    fmt.Println("score is", score)

    // ✅ Expected output:  score is 9
}

Now pass an address so a function can reset your variable.

package main

import "fmt"

func reset(n *int) {
    ___ = 0                // 👉 *n   (set the pointed-at value to 0)
}

func main() {
    // 🎯 YOUR TURN — pass the address so reset can change lives.
    lives := 3
    reset(___lives)        // 👉 &lives

    fmt.Println("lives:", lives)

    // ✅ Expected output:  lives: 0
}

🧩 Reorder Challenge

These lines change a variable through a pointer, but they're scrambled. Put them in order so the program prints 42 (assume a func main wraps them).

    *p = 42
    p := &x
    x := 1
    fmt.Println(x)

Why: x must exist before you can take its address. p := &x captures that address; *p = 42 writes through the pointer into x. Printing x last shows 42 because the write changed the original variable.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

OperationSyntaxMeaning
Address-ofp := &xp is *int pointing at x
Dereference (read)v := *pvalue at p
Dereference (write)*p = 9changes the original
Allocatep := new(int)zeroed *int
Zero valuevar p *intnil
Struct fieldp.Fieldauto-dereferenced

🧠 Quick Recall

Predict the output before you reveal the answer.

x := 5; p := &x; *p = 8; fmt.Println(x)

8 — p points at x, so writing *p = 8 changes the original x to 8.

func f(n int) { n = 0 }; x := 4; f(x); fmt.Println(x)

4 — f takes n by value, so it edits a copy. The original x is unchanged. (Pass *int to mutate it.)

var p *int; fmt.Println(p == nil)

true — the zero value of any pointer type is nil. Dereferencing it would panic, so always check first.

Frequently Asked Questions

Q: When should I use a pointer?

Two main reasons: to let a function mutate the caller's value, and to avoid copying a large struct on every call. For small, read-only values, a plain value is clearer.

Q: Is Go pointer code dangerous like C?

Much safer. There's no pointer arithmetic, memory is garbage-collected, and returning the address of a local variable is perfectly fine — Go keeps it alive for you.

Q: Why can I write p.Field instead of (*p).Field?

Go automatically dereferences a struct pointer when you access a field or call a method, purely as a convenience. Both forms mean the same thing.

Mini-Challenge: Deposit Through a Pointer

Write a deposit(a *Account, amount int) that adds to a.Balance, then call it with &acc. Because you pass the address, the change is visible in main. Write it yourself and match the example output.

package main

import "fmt"

type Account struct {
    Balance int
}

// 🎯 MINI-CHALLENGE: Deposit through a pointer
// Write a function deposit(a *Account, amount int) that adds
// amount to a.Balance. Because you pass a pointer, the change
// should be visible back in main.
//
// ✅ Example output:  balance: 250

func main() {
    acc := Account{Balance: 200}
    // call deposit here to add 50
    fmt.Println("balance:", acc.Balance)
}

🎉 Lesson Complete!

Practice quiz

What does the & operator do in Go?

  • Dereferences a pointer
  • Takes the address of a variable
  • Multiplies values
  • Declares a pointer type

Answer: Takes the address of a variable. &x yields a pointer holding the memory address of x.

What does the * operator do when applied to a pointer value?

  • Takes its address
  • Dereferences it to read or write the pointed-to value
  • Frees memory
  • Compares pointers

Answer: Dereferences it to read or write the pointed-to value. *p dereferences the pointer to access the value it points to.

What is the zero value of a pointer?

  • 0
  • nil
  • An empty struct
  • Undefined

Answer: nil. An uninitialized pointer is nil.

What happens if you dereference a nil pointer?

  • Returns 0
  • Returns nil
  • Runtime panic
  • Compile error

Answer: Runtime panic. Dereferencing a nil pointer causes a runtime panic (invalid memory address).

After p := &x; *p = 20, what is the value of x?

  • 1
  • 20
  • 0
  • nil

Answer: 20. Writing through the pointer with *p = 20 changes the original variable x to 20.

How can a function modify the caller's variable?

  • Pass it by value
  • Take a *T parameter and call with &x
  • Return it only
  • Use a global

Answer: Take a *T parameter and call with &x. Pass a pointer (*T) and call with the address (&x) so the function mutates the original.

What does new(T) return?

  • A value of type T
  • A pointer to a zeroed T
  • nil
  • An error

Answer: A pointer to a zeroed T. new(T) allocates a zeroed T and returns a *T pointing to it.

Does Go support pointer arithmetic like C (p++)?

  • Yes
  • No
  • Only for slices
  • Only with unsafe

Answer: No. Ordinary Go has no pointer arithmetic — pointers can't be incremented.

Given p *Account, how do you read a struct field through it?

  • (*p).Field only
  • p.Field — Go auto-dereferences
  • p->Field
  • Field(p)

Answer: p.Field — Go auto-dereferences. Go auto-dereferences struct pointers, so p.Field works directly.

Passing a large struct by pointer instead of by value mainly avoids:

  • Type errors
  • Copying the whole struct
  • Imports
  • Panics

Answer: Copying the whole struct. A pointer passes an address, avoiding a full copy of the struct's data.

Continue this course