Session 6 · Pointers#

Watch the class#

The big idea#

Every variable lives at an address in memory. A pointer is a variable that holds such an address. Why care? Because Go copies values when you pass them to a function. If you want the function to change the original, give it the address instead of a copy.

Everyday example: a document. Emailing a photocopy means edits stay on the copy. Sending the link to the shared file means everyone edits the same document. A pointer is the link.

In plain words

&x means “the address of x”. *p means “the thing p points to”. *int is the type “pointer to an int”. A pointer that points nowhere is nil.

Reach for it when:

  • a function must change its argument

  • a struct is large and copying it each call is wasteful

  • you need “no value” (a nil pointer) as a possible answer

Pass a copy, or pass the address By value the function changes its own copy. With a pointer it follows the address and changes the original. double(n) – by value double(&n) – by pointer n (main) 5 n (copy) 10 copied main still sees 5 n (main) 10 p (pointer) → &n *p = *p * 2 main now sees 10 x := 10 · p := &x · *p = 20 // x is now 20 &x = “the address of x” · *p = “the value p points at” A pointer you never set is nil – dereferencing nil panics
By value the function changes its own copy. With a pointer it follows the address and changes the original.

Address and value#

basics.go

package main

import "fmt"

func main() {
	x := 10
	p := &x // p holds the address of x
	fmt.Println(x, *p)
	fmt.Println(p == &x)

	*p = 20 // change x through p
	fmt.Println(x)

	var nothing *int
	fmt.Println(nothing == nil)
}
10 10
true
20
true

Pointers and functions#

functions.go

package main

import "fmt"

type User struct{ Name string }

func renameCopy(u User)     { u.Name = "Changed" }
func renameOriginal(u *User) { u.Name = "Changed" } // same as (*u).Name

func doubleCopy(n int)     { n = n * 2 }
func doubleOriginal(n *int) { *n = *n * 2 }

func main() {
	n := 5
	doubleCopy(n)
	fmt.Println(n)
	doubleOriginal(&n)
	fmt.Println(n)

	u := User{Name: "Syed"}
	renameCopy(u)
	fmt.Println(u.Name)
	renameOriginal(&u)
	fmt.Println(u.Name)

	// new gives a pointer to a zero value
	q := new(int)
	*q = 7
	fmt.Println(*q)
}
5
10
Syed
Changed
7
  • doubleCopy(n) received its own n, so main’s n stayed 5. doubleOriginal(&n) followed the address.

  • For structs Go lets you write u.Name on a pointer; it means (*u).Name automatically.

  • new(int) returns a pointer to a fresh zero int.

  • Slices, maps and channels already contain a reference inside, so you rarely need a pointer to one.

nil pointers#

nil_panic.go

package main

import "fmt"

type User struct{ Name string }

func greet(u *User) string {
	if u == nil {
		return "hello, stranger"
	}
	return "hello, " + u.Name
}

func main() {
	fmt.Println(greet(&User{"Asha"}))
	fmt.Println(greet(nil))

	defer func() { fmt.Println("recovered:", recover()) }()
	var u *User
	fmt.Println(u.Name) // nil pointer dereference
}
hello, Asha
hello, stranger
recovered: runtime error: invalid memory address or nil pointer dereference

Reading through a nil pointer crashes the program. Check if p == nil before you use a pointer you did not create yourself. The recover() here is only to show the message; normal code does not need it.

Common mistakes#

  • Dereferencing nil. The classic “invalid memory address” panic.

  • Expecting a function to change a plain argument. Without a pointer it works on a copy.

  • Pointers to everything. Use pointers when you need to change something or to avoid copying big values; small values are fine to copy.

  • Returning a pointer to a loop variable in old Go (before 1.22) shared one variable; modern Go makes a new one per iteration.

  • Confusing * in a type and * in an expression. In p *int it builds a type; in *p = 3 it follows the pointer.

Hands-on exercises#

Exercise 1 · Swap. Write swap(a, b *int) and use it on two variables.

Solution
package main

import "fmt"

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

func main() {
	x, y := 1, 2
	swap(&x, &y)
	fmt.Println(x, y)
}
2 1

Exercise 2 · Birthday. Write birthday(p *Person) that adds one to Age.

Solution
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func birthday(p *Person) {
	p.Age++
}

func main() {
	p := Person{"Asha", 28}
	birthday(&p)
	birthday(&p)
	fmt.Println(p)
}
{Asha 30}

Exercise 3 · Optional value. Write find(names []string, target string) *string that returns a pointer to the matching element or nil, and print a message for both outcomes.

Solution
package main

import "fmt"

func find(names []string, target string) *string {
	for i := range names {
		if names[i] == target {
			return &names[i]
		}
	}
	return nil
}

func main() {
	names := []string{"Asha", "Ravi"}
	for _, t := range []string{"Ravi", "Zoe"} {
		if p := find(names, t); p != nil {
			fmt.Println("found", *p)
		} else {
			fmt.Println(t, "not found")
		}
	}
}
found Ravi
Zoe not found

Return &names[i], not &n from a range loop copy.