Most People Don’t Understand Why Go Uses Pointers Instead of References

7 min readAryan

Why Should We Even Care About This?

When I first bumped into pointers in Go, I was totally thrown off. It’s a question tons of people ask when they start out. I came from playing around with Java and Python, where everything just magically works with references, and pointers felt like this scary throwback to C. Back in college, I’d tried C a bit, and it always felt like I was one wrong move away from breaking everything. So, seeing pointers in Go made me think, “Why would they do this? Isn’t this supposed to be simple?”

But then I stuck with it, and something clicked. Pointers aren’t just some old-school hassle — they’re there on purpose. They give you control over how memory works, which makes your code faster and way easier to follow once you get the hang of it. Honestly, I went from being confused to actually liking them. Today, I want to walk you through why Go picks pointers, how they help us out, and what they look like in action. If you’re writing Go — or even just curious — this stuff is worth knowing. It’s not as tricky as it seems, I promise!

What Are Pointers and References Anyway?

Pointers in Go, Made Simple

Okay, so what’s a pointer? It’s just a variable that tells you where some data lives in your computer’s memory. Think of it like knowing the exact shelf where your favorite book is. Say I write x := 15. A pointer to x knows where that 15 is hiding. We use & to grab that address and * to peek at or tweak the value. Check this out:

x := 15
p := &x
print(*p) # outputs 15
*p = 25
print(x) # now it’s 25

See? It’s straightforward. You point to the spot, you change what’s there, and you’re the boss. No weird surprises.

References in Other Places

Now, what about references? In Java, when you pass an object around, it’s a reference — you’re not copying the whole thing, just pointing to it in a sneaky way. The language handles it for you, and you don’t mess with addresses. C++ has references too — they’re like a nickname for a variable. It’s easy, but sometimes you’re left scratching your head wondering what changed. Go doesn’t play that game, and that’s where pointers come in.

Why Go Picks Pointers Every Time

So why does Go ditch references? First off, it’s all about being clear. With a pointer, you know you’re messing with the real data — not some copy that disappears. References can hide what’s going on, and I’ve seen that trip people up. Second, it’s about speed. Pointers let you skip copying big chunks of data, so Go runs quick without extra fuss. Third, it keeps things simple. References in other languages come with all these rules — like in Java or C++ — but Go’s pointers? They’re just there, doing their job. You get the power without the headache.

How Python Handles References

To really get why Go does this, let’s peek at Python. Everything in Python is a reference. Pass a list to a function? You’re passing a reference, not a copy. It’s handy, but it can bite you. Like, if you change that list inside the function, the original list changes too — sometimes when you don’t want it to. I’ve messed that up before! In Go, pointers make it obvious what’s changing. No guessing, just the code telling you straight up.

Where Pointers Save the Day

Fixing Structs That Won’t Change

Ever write a function to update a struct and it just… doesn’t? That’s because Go copies stuff when you pass it. Without a pointer, your changes vanish. Pointers fix that. Imagine a User struct with a name and age. No pointer? Nothing updates. With a pointer? Boom, it works.

Not Wasting Time on Copies

Copying big data is a drag. Picture a struct with a million numbers in a list. Pass it without a pointer, and Go makes a whole new copy — slow and wasteful. With a pointer, you just send the address. It’s tiny, fast, and perfect for stuff like servers where speed matters.

Changing Things Inside Functions

Sometimes you need a function to tweak multiple things. Without pointers, you’re stuck returning new values, which gets messy. Pointers keep it clean. Want to swap two numbers? Point to them, swap them, done.

Sharing Data with Goroutines

Go’s goroutines are awesome, and pointers make them even better. If a bunch of tasks need to update the same thing, a pointer lets them all hit the real data. Just lock it so they don’t step on each other. It’s a lifesaver in Go.

Checking If Something’s Missing

Pointers can also say, “Hey, there’s nothing here.” A *int can be nil—no value. A plain int can’t. That’s great for optional stuff, like when a function doesn’t have all the details yet.

Let’s Look at Some Code Now

A Basic Pointer to Start

Here’s a quick one. Let’s add 1 to a number:

func add_one(n *int) {
    *n = *n + 1
}

num := 7
add_one(&num)
print(num) # outputs 8

That *int says we’re using a pointer. &num gives the address. Inside, we bump it up. Simple and clear.

Pointers with Structs, Step by Step

Now, let’s try a struct. We’ve got a User and want to update it:

type User struct {
    name string
    age int
}

func make_older(u *User) {
    u.age = u.age + 1
}

func change_name(u *User, new_name string) {
    u.name = new_name
}

u := User{"Ravi", 30}
make_older(&u)
change_name(&u, "Ravi Kumar")
print(u.name) # outputs Ravi Kumar
print(u.age) # outputs 31

Without pointers, u stays stuck. Here, it updates because we used &u. I added the name change to show it off more.

Making Code Faster with Pointers

What if the data’s huge? Check this:

type BigList struct {
    values [1000000]int
}

func update_by_value(d BigList) {
    d.values[0] = 50
}

func update_by_pointer(d *BigList) {
    d.values[0] = 50
}

d := BigList{}
update_by_pointer(&d) # fast, no copy
print(d.values[0]) # outputs 50
update_by_value(d) # slow, copies everything

The pointer way is speedy because it skips the copy. Try it yourself — it’s a big difference!

Pointers with Goroutines, Real Example

Let’s use goroutines now. We’ll count up with tasks:

type Counter struct {
    count int
}

func increase(c *Counter) {
    c.count = c.count + 1
}

c := Counter{}
for i := 0; i < 20; i++ {
    go increase(&c)
}
time.Sleep(1 * time.Second) # wait a bit
print(c.count) # outputs around 20

It’s basic, and the number might jump around without a lock, but it shows how pointers let everyone update c.

Pointers for Missing Values

One more trick — pointers can show if something’s missing:

func check_age(age *int) string {
    if age == nil {
        return "no age given"
    }
    return "age is " + fmt.Sprint(*age)
}

var no_age *int
real_age := 40
print(check_age(no_age)) # outputs no age given
print(check_age(&real_age)) # outputs age is 40

This is nice when you don’t have all the pieces yet.

Working with Pointers and Slices

Slices in Go are a little special, so let’s talk about them. They’re reference types, meaning when you pass a slice to a function, it’s already pointing to the real array underneath. Change the slice in the function, and the original changes too. But sometimes, you still need pointers with slices — like if you want to mess with the slice itself, like its length.

Here’s an example:

func appendToSlice(s *[]int, value int) {
    *s = append(*s, value)
}

numbers := []int{1, 2, 3}
appendToSlice(&numbers, 4)
print(numbers) # outputs [1 2 3 4]

We use a pointer here so we can add to the slice inside the function. Without it, we’d just mess with a copy, and the original wouldn’t budge. It took me a minute to wrap my head around that, but it’s super useful once you see it.

Pointer Receivers in Methods

Pointers pop up in methods too, and they’re a big deal. In Go, methods have receivers — like the thing they’re tied to. That receiver can be a value or a pointer. With a pointer receiver, the method can change the actual thing.

Look at this:

type Person struct {
    name string
    age int
}

func (p *Person) haveBirthday() {
    p.age++
}

p := Person{"Sourabh", 25}
p.haveBirthday()
print(p.age) # outputs 26

Here, haveBirthday uses a pointer receiver, so it can bump up the age. If it was a value receiver, it’d just change a copy, and Sourabh would stay 25 forever. I use pointer receivers a lot when I need to update stuff or when copying would slow things down.

Let’s Finish This Up with Some Tips

Pointers in Go might feel weird at first. I know they did for me. But they’re honestly awesome once you get comfy with them. Here’s some stuff I’ve learned along the way:

  • Use pointers when you need to change the original data in a function. It’s the only way to make it stick.
  • Skip pointers if you’re just reading stuff — passing by value is safer and less to worry about.
  • Watch out for nil pointers! Check if they’re nil before you use them, or you’ll crash.
  • Slices, maps, and channels are already reference-y, so you might not need pointers unless you’re tweaking the reference itself.

Getting into pointers has made my Go code so much cleaner and quicker. It’s like a little superpower where you decide exactly what happens with your data. Next time you’re coding in Go, don’t dodge pointers. Mess around with them, try them in different spots, and you’ll see why they’re great. What do you think? Have pointers ever saved your day, or totally confused you? I’d love to hear about it!

More in programming

Cubed

Write about the technologies shaping the future.

For developers, founders, and curious minds exploring AI, crypto, Web3, and emerging tech—signal over noise.

One free account across In Plain English, Stackademic, Venture, and Cubed.

How it works
  • AI, crypto & Web3
  • Software & emerging technologies
  • Analysis & practical resources
  • Thoughtful voices, not hype
1

Sign in

Google or GitHub

2

Complete profile

Takes a few minutes

3

Get approved & publish

Start sharing

Why write for Cubed?

The future deserves thoughtful voices, not just louder headlines.

Comments

Loading comments…

Posts Across the Network