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
-
&(Address-of): This operator finds where a variable is sitting in memory. -
*(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
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
}
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"
- 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"
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)
}
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)