In the previous tutorial, you learned about interfaces and polymorphism. Now it is time to learn about pointers — how Go lets you share and modify data efficiently.

If you have used C or C++, you know pointers. Go pointers are simpler. There is no pointer arithmetic. You cannot do math on pointer addresses. This makes Go pointers safe and easy to understand.

What is a Pointer?

A pointer holds the memory address of a value. Instead of copying the value, you pass its address. This lets functions modify the original value:

package main

import "fmt"

func main() {
    name := "Alex"

    // & gives the address of a variable
    ptr := &name
    fmt.Println("Value:", name)        // Alex
    fmt.Println("Address:", ptr)       // 0xc000014070 (some memory address)
    fmt.Println("Type:", fmt.Sprintf("%T", ptr)) // *string

    // * reads the value at the address (dereference)
    fmt.Println("Dereferenced:", *ptr) // Alex
}

Output:

Value: Alex
Address: 0xc000014070
Type: *string
Dereferenced: Alex

Two operators to remember:

  • & — “address of” — gives you a pointer to a variable
  • * — “dereference” — gives you the value a pointer points to

Modifying Values Through Pointers

The main reason to use pointers is to modify a value from a different function:

package main

import "fmt"

// Without pointer — gets a COPY, changes are lost
func doubleValue(n int) {
    n = n * 2
    fmt.Println("Inside doubleValue:", n) // 20
}

// With pointer — modifies the ORIGINAL
func doublePointer(n *int) {
    *n = *n * 2
    fmt.Println("Inside doublePointer:", *n) // 20
}

func main() {
    num := 10

    doubleValue(num)
    fmt.Println("After doubleValue:", num) // 10 — unchanged

    doublePointer(&num)
    fmt.Println("After doublePointer:", num) // 20 — changed
}

Output:

Inside doubleValue: 20
After doubleValue: 10
Inside doublePointer: 20
After doublePointer: 20

Without a pointer, the function works on a copy. With a pointer, it modifies the original. This is important for functions that need to update the caller’s data.

Pointers with Structs

Pointers are commonly used with structs. Structs can be large, so passing a pointer avoids copying all the data:

package main

import "fmt"

type User struct {
    Name  string
    Email string
    Age   int
}

// Accepts a pointer — can modify the user
func celebrateBirthday(u *User) {
    u.Age++
    fmt.Printf("Happy birthday, %s! You are now %d.\n", u.Name, u.Age)
}

// Accepts a value — gets a copy
func printUser(u User) {
    fmt.Printf("User: %s (%s, age %d)\n", u.Name, u.Email, u.Age)
}

func main() {
    user := User{Name: "Alex", Email: "alex@example.com", Age: 25}
    printUser(user)

    celebrateBirthday(&user)
    printUser(user)
}

Output:

User: Alex (alex@example.com, age 25)
Happy birthday, Alex! You are now 26.
User: Alex (alex@example.com, age 26)

Notice: Go automatically dereferences struct pointers. You write u.Age, not (*u).Age. Both work, but the short form is standard.

Creating Structs with Pointers

You can create a pointer to a struct directly with &:

package main

import "fmt"

type Config struct {
    Host string
    Port int
}

func NewConfig(host string, port int) *Config {
    return &Config{
        Host: host,
        Port: port,
    }
}

func main() {
    cfg := NewConfig("localhost", 8080)
    fmt.Printf("Host: %s, Port: %d\n", cfg.Host, cfg.Port)
}

This is the common pattern for constructor functions in Go. Return a *Config (pointer), not a Config (value).

Pointer Receivers on Methods

You already saw pointer receivers in the structs tutorial. Here is a deeper look:

package main

import "fmt"

type Stack struct {
    items []int
}

// Pointer receiver — modifies the stack
func (s *Stack) Push(item int) {
    s.items = append(s.items, item)
}

// Pointer receiver — modifies the stack
func (s *Stack) Pop() (int, bool) {
    if len(s.items) == 0 {
        return 0, false
    }
    last := len(s.items) - 1
    item := s.items[last]
    s.items = s.items[:last]
    return item, true
}

// Value receiver — only reads the stack
func (s Stack) Peek() (int, bool) {
    if len(s.items) == 0 {
        return 0, false
    }
    return s.items[len(s.items)-1], true
}

// Value receiver — only reads
func (s Stack) Size() int {
    return len(s.items)
}

func main() {
    var s Stack

    s.Push(10)
    s.Push(20)
    s.Push(30)
    fmt.Println("Size:", s.Size())

    if val, ok := s.Peek(); ok {
        fmt.Println("Top:", val)
    }

    for s.Size() > 0 {
        if val, ok := s.Pop(); ok {
            fmt.Printf("Popped: %d\n", val)
        }
    }
    fmt.Println("Size after popping all:", s.Size())
}

Output:

Size: 3
Top: 30
Popped: 30
Popped: 20
Popped: 10
Size after popping all: 0

Rule of thumb: If any method needs a pointer receiver, use pointer receivers for all methods on that type. This avoids confusion.

nil Pointers

A pointer that does not point to anything has the value nil:

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

func main() {
    var ptr *int
    fmt.Println("Nil pointer:", ptr) // <nil>

    // Check for nil before using
    if ptr != nil {
        fmt.Println("Value:", *ptr)
    } else {
        fmt.Println("Pointer is nil")
    }

    // Practical example: linked list
    node3 := &Node{Value: 3, Next: nil}
    node2 := &Node{Value: 2, Next: node3}
    node1 := &Node{Value: 1, Next: node2}

    // Walk the linked list
    current := node1
    for current != nil {
        fmt.Printf("%d -> ", current.Value)
        current = current.Next
    }
    fmt.Println("nil")
}

Output:

Nil pointer: <nil>
Pointer is nil
1 -> 2 -> 3 -> nil

Important: Dereferencing a nil pointer causes a panic (crash):

var ptr *int
fmt.Println(*ptr) // panic: runtime error: invalid memory address or nil pointer dereference

Always check for nil before dereferencing a pointer.

The new Function

The new function allocates memory and returns a pointer. The value is set to its zero value:

package main

import "fmt"

func main() {
    // new returns a pointer to a zero-valued int
    ptr := new(int)
    fmt.Println("Value:", *ptr) // 0
    fmt.Println("Type:", fmt.Sprintf("%T", ptr)) // *int

    *ptr = 42
    fmt.Println("Updated:", *ptr) // 42

    // For structs, &Type{} is more common than new(Type)
    type Point struct{ X, Y int }

    p1 := new(Point)        // Less common
    p2 := &Point{X: 1, Y: 2} // More common

    fmt.Println("p1:", *p1)
    fmt.Println("p2:", *p2)
}

In practice, most Go developers use &Type{} instead of new(Type) for structs. The new function is more common for basic types.

When to Use Pointers vs Values

Here is a simple guide:

Use pointers when:

  • The function needs to modify the caller’s data
  • The struct is large (avoids copying)
  • You need to represent “no value” with nil
  • Consistency — if some methods use pointer receivers, all should

Use values when:

  • The data is small (int, bool, small structs)
  • You want immutability — the function should not modify the original
  • The type is a map, slice, or channel (they are already reference types)
package main

import "fmt"

type SmallPoint struct {
    X, Y int
}

type LargeData struct {
    Name    string
    Records [1000]int
    Buffer  [4096]byte
}

// Value — SmallPoint is small, no need for a pointer
func distance(p SmallPoint) float64 {
    return float64(p.X*p.X + p.Y*p.Y)
}

// Pointer — LargeData is big, avoid copying
func process(d *LargeData) {
    d.Name = "processed"
    fmt.Println("Processing:", d.Name)
}

func main() {
    point := SmallPoint{X: 3, Y: 4}
    fmt.Printf("Distance: %.1f\n", distance(point))

    data := LargeData{Name: "raw"}
    process(&data)
    fmt.Println("After process:", data.Name)
}

Note about slices and maps: Slices and maps are already reference types internally. When you pass a slice to a function, the function can modify the slice’s elements. You only need a pointer if you want to change the slice itself (like appending to it):

package main

import "fmt"

// No pointer needed — can modify elements
func doubleAll(nums []int) {
    for i := range nums {
        nums[i] *= 2
    }
}

// Pointer needed — modifies the slice itself
func addElement(nums *[]int, val int) {
    *nums = append(*nums, val)
}

func main() {
    numbers := []int{1, 2, 3}
    doubleAll(numbers)
    fmt.Println("Doubled:", numbers) // [2 4 6]

    addElement(&numbers, 100)
    fmt.Println("Added:", numbers) // [2 4 6 100]
}

No Pointer Arithmetic

In C, you can do math on pointers to walk through memory. Go does not allow this:

// This does NOT work in Go
ptr := &someArray[0]
ptr++ // Compile error: cannot do pointer arithmetic

This is a safety feature. Pointer arithmetic is a common source of bugs in C and C++. Go removes it entirely.

Go Pointers vs Rust Borrowing

If you are also learning Rust (from the Rust Tutorial series), here is how they compare:

FeatureGoRust
Syntax*T pointer, &x address&T reference, &mut T mutable reference
Null safetynil pointers (runtime panic)No null (Option<T> instead)
Pointer arithmeticNot allowedNot allowed (in safe Rust)
Multiple mutable referencesAllowed (programmer’s responsibility)Not allowed (enforced at compile time)
Garbage collectionYesNo (ownership system)

Go is simpler. You get pointers and the garbage collector handles memory. Rust is stricter. The borrow checker prevents data races at compile time but has a steeper learning curve.

For more on Rust’s approach, see Rust Tutorial #5: Borrowing and References.

A Complete Example

Here is a program that uses pointers in a practical way:

package main

import "fmt"

type TreeNode struct {
    Value int
    Left  *TreeNode
    Right *TreeNode
}

// Insert adds a value to the binary search tree
func (n *TreeNode) Insert(val int) {
    if val < n.Value {
        if n.Left == nil {
            n.Left = &TreeNode{Value: val}
        } else {
            n.Left.Insert(val)
        }
    } else {
        if n.Right == nil {
            n.Right = &TreeNode{Value: val}
        } else {
            n.Right.Insert(val)
        }
    }
}

// InOrder prints the tree in sorted order
func (n *TreeNode) InOrder() {
    if n == nil {
        return
    }
    n.Left.InOrder()
    fmt.Printf("%d ", n.Value)
    n.Right.InOrder()
}

// Search checks if a value exists
func (n *TreeNode) Search(val int) bool {
    if n == nil {
        return false
    }
    if val == n.Value {
        return true
    }
    if val < n.Value {
        return n.Left.Search(val)
    }
    return n.Right.Search(val)
}

func main() {
    fmt.Println("=== GO-9: Pointers ===")
    fmt.Println()

    // Build a binary search tree
    root := &TreeNode{Value: 50}
    values := []int{30, 70, 20, 40, 60, 80}
    for _, v := range values {
        root.Insert(v)
    }

    // Print in sorted order
    fmt.Print("In-order traversal: ")
    root.InOrder()
    fmt.Println()

    // Search for values
    fmt.Println()
    for _, v := range []int{40, 55, 70} {
        if root.Search(v) {
            fmt.Printf("Found %d in the tree\n", v)
        } else {
            fmt.Printf("%d is NOT in the tree\n", v)
        }
    }
}

Output:

=== GO-9: Pointers ===

In-order traversal: 20 30 40 50 60 70 80

Found 40 in the tree
55 is NOT in the tree
Found 70 in the tree

This example shows pointers in their natural habitat: tree data structures where nodes point to other nodes.

Common Mistakes

1. Dereferencing a nil pointer.

var user *User
fmt.Println(user.Name) // panic!

// Always check for nil
if user != nil {
    fmt.Println(user.Name)
}

This is the most common pointer mistake in Go. Always check for nil before using a pointer.

2. Returning a pointer to a local variable (this is actually fine in Go).

// This is SAFE in Go — the compiler moves the variable to the heap
func newUser() *User {
    user := User{Name: "Alex"}
    return &user // Safe! Go's escape analysis handles this
}

In C, returning a pointer to a local variable is dangerous. In Go, it is safe. The compiler detects this and allocates the variable on the heap instead of the stack.

3. Unnecessary pointers for small types.

// Unnecessary — int is small, just copy it
func process(n *int) { ... }

// Better — just pass the value
func process(n int) int { ... }

Do not use pointers for small types unless you need to modify the original. Passing a copy is fast and simpler.

Source Code

You can find the complete source code for this tutorial on GitHub:

GO-9 Source Code on GitHub

What’s Next?

In the next tutorial, Go Tutorial #10: Project Structure and Clean Architecture, you will learn:

  • The standard Go project layout (cmd/, internal/, pkg/)
  • How to organize packages
  • Dependency injection without a framework
  • The repository and service layer patterns
  • When to split code into packages vs keep it simple

This is part 9 of the Go Tutorial series. Follow along to learn Go from scratch.