One of the hardest things I found in trying to learn Go – this before I got to Concurrency and Go Routines – was the idea of Pointers. I understood that they point to location of a variable, rather than its value. What I didn’t understand was – why are pointers useful? So what – you can point to a variable’s location rather than its value – but how does that help in any practical way and why does it figure so prominently in every tutorial on Go?
I came to GO from Javascript, PHP and Python, none of which explicitly use Pointers (though as we’ll see below, Python does use a form of Pointers).
Pointers are relatively easy to create and use in Go. The syntax we use to define a pointer is:
var pointerName *dataTypeThe pointerName is the name of the variable that will hold our pointer. The dataType is the type that we associate with the value stored in the pointer. Here is a simple example of creating a pointer in the default main.go file:
package main
import "fmt"
func main() {
var a = 10 // variable a stores value of 10
var b *int = &a // variable b stores the memory address (location) but NOT the value of variable a
}
fmt.Println(a) // prints '10'// our value
fmt.Println(b) // prints '0xc0000aa058', the location of our variableSo simple enough to get my head around, but I still didn’t understand why pointers were such a key part of the Go language. To understand their practical applications, I asked, as one does, the LLMs, specifically Gemini Pro, which I’ve been using more and more lately. Some of its response was reproduced below:
Here are the three practical reasons why we use pointers in Go.
1. Mutability (Changing the Original)
Because Go copies everything by default, a function cannot change the state of a variable passed to it unless we give it the “address” (pointer) of that variable.
The “Oops” moment (Pass by Value):
Go
func changeName(name string) {
name = "Alice" // This only changes the COPY inside this function
}
func main() {
user := "Bob"
changeName(user)
fmt.Println(user) // Output: Bob (Nothing happened!)
}
The Fix (Pass by Pointer):
Go
// We accept a pointer to a string (*string)
func changeName(name *string) {
*name = "Alice" // We travel to the address and update the value there
}
func main() {
user := "Bob"
changeName(&user) // We pass the address (&)
fmt.Println(user) // Output: Alice
}
Practical Application: We see this everywhere in Go methods: if a method needs to update the state of a struct (like setting a User.IsLoggedIn = true), it must use a pointer receiver.
2. Performance (Efficiency)
Imagine we have a struct that represents a massive JPEG image or a user profile with 50 fields.
- Without Pointers: Every time we pass that struct to a function, Go has to pause and copy every single bit of that data into a new location. This eats up memory and CPU.
- With Pointers: We pass the memory address. An address is tiny (usually 8 bytes on a 64-bit system).
Python, by contrast, does this for us automatically. It never copies the whole image object; it just passes the reference. In Go, you have to choose to do this manually by using a pointer.
Rule of Thumb: If the data is small (like an integer or a coordinate point), copy it. If it is large (like a User struct or a config object), use a pointer to share it without copying.
3. Representing “Null” (Absence of Value)
This is a massive one for web development (SQL/JSON).
In Go, types have “zero values.”
- The zero value of an
intis0. - The zero value of a
stringis"".
The Problem:
If a user submits a JSON form and the “age” field is 0, did they enter 0, or did they just leave it blank? In standard Go, we can’t tell.
The Solution:
Pointers can be nil.
var age int-> Default is0.var age *int-> Default isnil.
If age is nil, the user left it blank. If age points to a value of 0, then we know the user explicitly typed 0.
Practical Application: We see this quite a lot in database structs.
Go
type User struct {
Name string
DeletedAt *time.Time // Pointer allows this to be null (user is active)
}
If DeletedAt wasn’t a pointer, it would default to the year 0001-01-01, which is confusing data.
Summary Table
| Feature | Pass by Value (No Pointer) | Pass by Pointer (*) |
| Analogy | Sending a friend a photocopy of a map. | Sending a friend the GPS coordinates. |
| Mutability | They can draw on the map, but your map stays clean. | If they change the location, it changes for you too. |
| Performance | Slow for large items (requires photocopying). | Fast (just sending coordinates). |
| Safety | Safe. Original data cannot be corrupted. | Risky. Original data can be modified or be nil. |
A Note on Python
Python, it seems, does use pointers exclusively for objects (Lists, Dicts, Classes)—it just hides the syntax from you (* and &).
- Python:
a = [1, 2]; b = a(b is now a pointer to a). - Go:
a := []int{1, 2}; b := a(b is a pointer to the underlying array).*
(Additional Note: Go Slices and Maps are special types that implicitly act like pointers, which helps make Go feel friendlier, but Structs do not.)