Pointers

Go · Lesson 17 of 32 6 min read Updated

Every variable lives somewhere in the computer's memory, and that place has an address. A pointer is a value that holds such an address. With a pointer, a function can change a variable that belongs to its caller, and big values can be shared without copying.

Pointers scare many students who learned them in C. Go's pointers are much safer: there is no pointer arithmetic, memory is freed automatically, and the compiler handles the tricky parts. In this lesson you will learn the two pointer operators, use pointers with functions and structs, and know when not to use them.

& and *

Two operators do all the work:

  • &x gives the address of the variable x. Read it as "address of x".
  • *p follows the pointer p to the value it points at. Read it as "the value at p".

The type of a pointer to an int is written *int.

Go
package main

import "fmt"

func main() {
	score := 72
	p := &score // p holds the address of score

	fmt.Println(*p)
	fmt.Printf("%T\n", p)

	*p = 85 // change score through the pointer
	fmt.Println(score)
	fmt.Println(p == &score)
}
Output
72
*int
85
true

p and &score hold the same address, so writing to *p changed score itself. If you print p directly you will see an address such as 0xc000012345; it differs on every run, so do not depend on it.

Changing the caller's variable

Go passes arguments by value: a function gets a copy. To let a function change the caller's variable, pass a pointer:

Go
package main

import "fmt"

func addGraceCopy(marks int) {
	marks += 5 // changes only the local copy
}

func addGrace(marks *int) {
	*marks += 5 // changes the caller's variable
}

func main() {
	rohan := 35
	addGraceCopy(rohan)
	fmt.Println("After copy version:", rohan)

	addGrace(&rohan)
	fmt.Println("After pointer version:", rohan)
}
Output
After copy version: 35
After pointer version: 40

The caller writes &rohan to hand over the address. The *int in the function signature tells every reader "this function may change your value".

nil pointers and optional values

The zero value of a pointer is nil: it points at nothing. Following a nil pointer stops the program with panic: runtime error: invalid memory address or nil pointer dereference. Always check for nil when a pointer may be empty.

That "maybe empty" property is useful. A pointer can say "this value was not given", which is different from "the value is zero":

Go
package main

import "fmt"

func describe(discount *int) string {
	if discount == nil {
		return "no discount set"
	}
	return fmt.Sprintf("discount %d%%", *discount)
}

func main() {
	var notSet *int
	fmt.Println(notSet == nil, describe(notSet))

	zero := 0
	fmt.Println(describe(&zero))

	fmt.Println(describe(new(15)))
}
Output
true no discount set
discount 0%
discount 15%

The last line uses new. The built-in new(T) creates a variable of type T with its zero value and returns a pointer to it. Since Go 1.26, new also accepts an expression: new(15) creates an int variable holding 15 and returns its address, so you no longer need a temporary variable just to take its address.

Pointers to structs

You will learn structs fully in the next lesson; for now, a struct is a type that groups named fields. Pointers to structs are very common, and Go lets you write p.Field instead of the longer (*p).Field:

Go
package main

import "fmt"

type Account struct {
	Holder  string
	Balance int
}

func deposit(a *Account, amount int) {
	a.Balance += amount // Go reads this as (*a).Balance
}

func main() {
	acc := Account{Holder: "Farhan", Balance: 1000}
	deposit(&acc, 500)
	fmt.Println(acc.Holder, acc.Balance)

	same := &acc
	same.Balance -= 200
	fmt.Println(acc.Balance)
}
Output
Farhan 1500
1300

Returning a pointer to a local variable

In C, returning the address of a local variable is a serious bug. In Go it is completely safe. The compiler notices that the variable is still needed after the function returns and keeps it alive (it "escapes to the heap"):

Go
package main

import "fmt"

func newCounter() *int {
	n := 0
	return &n
}

func main() {
	c := newCounter()
	*c++
	*c++
	fmt.Println(*c)
}
Output
2

*c++ means "increase the value that c points at". The garbage collector frees n once nothing points at it any more.

When to use pointers

Use a pointer when:

  • a function must change the caller's value;
  • a struct is large and copying it on every call would be wasteful;
  • you need "no value" (nil) as a possible state;
  • a type has methods that change it (pointer receivers, next lesson).

Do not use pointers to slices or maps just to share them. A slice already refers to a shared array, and a map already refers to shared data, so a function can change their elements without a pointer. The exception is when the function changes the slice's length with append: then return the new slice, as append itself does.

Common mistakes

  • Dereferencing a nil pointer. Check if p != nil before *p when the pointer may be empty.
  • Forgetting & at the call. Passing rohan where *int is expected is a compile error; write &rohan.
  • Using pointers everywhere "for speed". Small values like int or small structs are cheap to copy, and copies are easier to reason about.
  • Appending inside a function and expecting the caller's slice to grow. Return the new slice instead.

Key points

  • &x takes the address of x; *p reads or writes the value at that address; *T is the pointer type.
  • Go passes arguments by value; pass a pointer to let a function change the caller's variable.
  • A nil pointer points at nothing; dereferencing it panics.
  • new(T) gives a pointer to a zero value; Go 1.26 new(expr) gives a pointer to that value.
  • Struct fields work through pointers with p.Field, and returning a pointer to a local is safe.

Exercise

Write a function swap(a, b *int) that exchanges two integers through their pointers, and a function applyDiscount(price *int, percent int) that reduces a price in place. In main, swap Priya's and Meera's seat numbers (12 and 31) and apply a 20% discount to a ₹1,500 ticket, printing the results.

Show solution
Go
package main

import "fmt"

func swap(a, b *int) {
	*a, *b = *b, *a
}

func applyDiscount(price *int, percent int) {
	*price -= *price * percent / 100
}

func main() {
	priya, meera := 12, 31
	swap(&priya, &meera)
	fmt.Println("Priya:", priya, "Meera:", meera)

	ticket := 1500
	applyDiscount(&ticket, 20)
	fmt.Println("Ticket:", ticket)
}
Output
Priya: 31 Meera: 12
Ticket: 1200

Both functions receive addresses, so their changes land in main's variables. 20% of 1,500 is 300, leaving 1,200.

Quick Quiz

1. What does &x give you?

2. After n := 5; p := &n; *p = 9, what is n?

3. What happens when you dereference a nil pointer?

4. What does new(15) return in Go 1.26 and later?

Learn Go with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call