Back To Blog Page

Pointers in GO

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 *dataType

The 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 variable

So 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 int is 0.
  • The zero value of a string is "".

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 is 0.
  • var age *int -> Default is nil.

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

FeaturePass by Value (No Pointer)Pass by Pointer (*)
AnalogySending a friend a photocopy of a map.Sending a friend the GPS coordinates.
MutabilityThey can draw on the map, but your map stays clean.If they change the location, it changes for you too.
PerformanceSlow for large items (requires photocopying).Fast (just sending coordinates).
SafetySafe. 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.)


.entry-footer

Leave a Reply

Your email address will not be published. Required fields are marked *