DEV Community

Cover image for Understanding Pointers in GO
Joshua Olajide
Joshua Olajide

Posted on

Understanding Pointers in GO

Introduction

Pointers are one of GO's most elegant features. Understanding them is the difference between writing code that just works and writing code that is efficient and also predictable.

In this article, we’ll demystify pointers using simple mental models and a real-world project example.

How it works

According to the official Go Documentation, a pointer holds the memory address of a value.

To visualize this, imagine you have a house:

  • The Value: This is the physical house itself.
  • The Pointer: This is the address of the house written on a sticky note.

If you give someone the sticky note (the pointer), they can go to your house and paint the front door red. If you don't give them the address, you are essentially sending them a photocopy (a copy) of a picture of your house. They can paint that photocopy all they want, but your actual house stays exactly the same.

The & and * operators

Go uses two primary operators to handle pointers. They may look confusing at first, but they have very specific functions

  1. & (Address-of): This operator finds where a variable is sitting in memory.
  2. * (Dereference): This operator lets you follow the address to see (or change) the actual value inside.
var p *int // The type *int means p is a pointer to an integer
Enter fullscreen mode Exit fullscreen mode
package main

import "fmt"

func main() {
    name := "Alice"

    // & gets the memory address
    pointer := &name
    fmt.Println("Address:", pointer)   // Output: 0xc000014070

    // * accesses the value at that address
    fmt.Println("Value:", *pointer)    // Output: Alice
}
Enter fullscreen mode Exit fullscreen mode

Copying vs. Pointing

Why does this matter? It comes down to how Go handles data in functions.

  • Pass-by-Value (Copying) When you pass a variable to a function in Go, it creates a copy by default. Changes inside the function stay inside the function.
func changeName(n string) {
    n = "Bob" // This only updates the copy!
}

// Result: original 'name' remains "Alice"
Enter fullscreen mode Exit fullscreen mode
  • Pass-by-Pointer (Pointing) When you pass a pointer, the function modifies the data at the source.
func changeName(n *string) {
    *n = "Bob" // This follows the address and changes the original
}

// Result: original 'name' becomes "Bob"
Enter fullscreen mode Exit fullscreen mode

Real World Scenario: The Food Ordering System

In a real system, you don't want to keep creating copies of a user's order every time they add an item that would be slow and lead to data bugs. You want one source of truth.

package main

import "fmt"

type Order struct {
    CustomerName string
    Items        []string
    TotalPrice   float64
    IsDiscounted bool
}

// We use *Order to ensure we are updating the actual order
func AddItem(order *Order, item string, price float64) {
    order.Items = append(order.Items, item)
    order.TotalPrice += price
    fmt.Printf("Added %s ($%.2f)\n", item, price)
}

func ApplyDiscount(order *Order, percent float64) {
    if order.IsDiscounted {
        fmt.Println("Discount already applied!")
        return
    }
    discount := order.TotalPrice * (percent / 100)
    order.TotalPrice -= discount
    order.IsDiscounted = true
    fmt.Printf("%.0f%% discount applied!\n", percent)
}

func main() {
    myOrder := Order{CustomerName: "John"}

    // Pass the address of myOrder using &
    AddItem(&myOrder, "Burger", 8.99)
    AddItem(&myOrder, "Fries", 3.49)
    ApplyDiscount(&myOrder, 10)

    fmt.Printf("Final Total for %s: $%.2f\n", myOrder.CustomerName, myOrder.TotalPrice)
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Pointers (Memory Addresses) point to where data lives, not the data itself.

  • Pointers are great for large structs (like our Order) because you aren't copying the whole object every time you call a function.

  • Use & to create a pointer and * to read/edit the value it points to.

  • In Go, the zero value of a pointer is nil. Always ensure a pointer isn't nil before dereferencing it to avoid crashes.

PS: If your struct is small (like a single int), just pass the value. Use pointers when you need to modify the data or when the data is large enough that copying it would consume unnecessary memory.

Top comments (0)