DEV Community

Cover image for Learning Go as a Ruby Developer #7: Finally Understanding Pointers
Shrouk Abozeid
Shrouk Abozeid

Posted on AI-assisted

Learning Go as a Ruby Developer #7: Finally Understanding Pointers

Pointers were probably one of the concepts I was most curious about when I started learning Go.

Coming from Ruby, I don't normally think about memory addresses or explicitly passing references around.

In Go, pointers are much more visible.

At first, they looked like one of those concepts I remembered from C++ and hoped I wouldn't have to think about again.

But once I understood what they actually represent, they became much less scary.


So What Is a Pointer?

A pointer is a value that stores the memory address of another value.

For example:

age := 30

pointer := &age
Enter fullscreen mode Exit fullscreen mode

The & operator gives us the address of age.

So instead of:

age
Enter fullscreen mode Exit fullscreen mode

which represents the value:

30
Enter fullscreen mode Exit fullscreen mode

we have:

pointer
Enter fullscreen mode Exit fullscreen mode

which represents the address where 30 is stored.


Getting the Value Back

If we have a pointer, we can use * to get the value stored at that address.

age := 30

pointer := &age

fmt.Println(pointer)
fmt.Println(*pointer)
Enter fullscreen mode Exit fullscreen mode

The first Println gives us the memory address.

The second gives us:

30
Enter fullscreen mode Exit fullscreen mode

because *pointer means:

Go to the address stored in pointer and give me the value there.

This is called dereferencing.

So we have two operators to remember:

& → get the address
* → get the value at that address
Enter fullscreen mode Exit fullscreen mode

Changing a Value Through a Pointer

This is where pointers become really useful.

age := 30

pointer := &age

*pointer = 31

fmt.Println(age)
Enter fullscreen mode Exit fullscreen mode

The result is:

31
Enter fullscreen mode Exit fullscreen mode

We didn't directly change age.

We changed the value at the address stored in pointer.

Since pointer points to age, the original value changes.


Pointers with Functions

This becomes especially useful when passing values to functions.

Consider this function:

func changeAge(age int) {
    age = 31
}
Enter fullscreen mode Exit fullscreen mode

If we do:

age := 30

changeAge(age)

fmt.Println(age)
Enter fullscreen mode Exit fullscreen mode

the value is still:

30
Enter fullscreen mode Exit fullscreen mode

That's because the function receives a copy of the value.

If we want the function to modify the original value, we can pass a pointer:

func changeAge(age *int) {
    *age = 31
}
Enter fullscreen mode Exit fullscreen mode

Then:

age := 30

changeAge(&age)

fmt.Println(age)
Enter fullscreen mode Exit fullscreen mode

Now the result is:

31
Enter fullscreen mode Exit fullscreen mode

We pass the address:

&age
Enter fullscreen mode Exit fullscreen mode

and inside the function we dereference it:

*age
Enter fullscreen mode Exit fullscreen mode

This Is Where Ruby Feels Very Different

In Ruby, I don't normally have to think about passing a memory address.

For example:

def change_name(user)
  user.name = "Alice"
end
Enter fullscreen mode Exit fullscreen mode

If user is an object, the method can modify that object.

Ruby handles the object references for me.

Go makes the relationship much more explicit.

I have to decide whether I'm passing the value itself:

func changeAge(age int)
Enter fullscreen mode Exit fullscreen mode

or a pointer to the value:

func changeAge(age *int)
Enter fullscreen mode Exit fullscreen mode

That distinction matters.


Pointers and Structs

Pointers become especially common when working with structs.

Let's use the Person struct from the previous article:

type Person struct {
    Name string
    Age  int
}
Enter fullscreen mode Exit fullscreen mode

We can create a pointer to a Person:

person := &Person{
    Name: "Alice",
    Age:  30,
}
Enter fullscreen mode Exit fullscreen mode

Now person is a pointer to a Person.

We can still access its fields directly:

fmt.Println(person.Name)
Enter fullscreen mode Exit fullscreen mode

Go automatically dereferences the pointer when accessing the field.

So:

person.Name
Enter fullscreen mode Exit fullscreen mode

is effectively shorthand for:

(*person).Name
Enter fullscreen mode Exit fullscreen mode

This is a nice convenience because we don't have to write * every time we access a field.


Pointer Receivers

This also connects to something we saw in Part 3: methods.

Remember that Go methods can have a receiver:

func (p Person) SayHello() {
    fmt.Println("Hello", p.Name)
}
Enter fullscreen mode Exit fullscreen mode

This is a value receiver.

The method receives a copy of the Person.

If we want the method to modify the original Person, we can use a pointer receiver:

func (p *Person) Rename(name string) {
    p.Name = name
}
Enter fullscreen mode Exit fullscreen mode

Now:

person := Person{
    Name: "Alice",
    Age:  30,
}

person.Rename("Bob")

fmt.Println(person.Name)
Enter fullscreen mode Exit fullscreen mode

prints:

Bob
Enter fullscreen mode Exit fullscreen mode

The pointer receiver allows the method to modify the original value.


Value Receiver vs Pointer Receiver

This distinction is important:

func (p Person) Rename(name string)
Enter fullscreen mode Exit fullscreen mode

means the method receives a copy.

While:

func (p *Person) Rename(name string)
Enter fullscreen mode Exit fullscreen mode

means the method receives a pointer to the original value.

Coming from Ruby, this is one of those things I don't normally have to think about.

In Ruby, I can simply mutate the object:

def rename(person)
  person.name = "Bob"
end
Enter fullscreen mode Exit fullscreen mode

Go makes the decision more explicit.


Go vs Ruby

Concept Go Ruby
Reference to a value Pointer Object reference handled by Ruby
Get address &value No equivalent syntax
Dereference *pointer No explicit dereference
Modify through reference *pointer = value Mutate the object directly
Pointer receiver func (p *Person) No equivalent
Value receiver func (p Person) No direct equivalent
Memory address Explicitly accessible through pointers Normally hidden

What Surprised Me

The syntax wasn't actually the hardest part.

It was changing the way I think about values.

In Ruby, I can mostly think:

"I have an object."

In Go, I sometimes have to ask:

"Do I have the value, or do I have a pointer to the value?"

That distinction affects how data is passed to functions and how methods can modify it.

Once I started thinking about a pointer as simply:

"A value that tells me where another value lives"

the concept became much easier.


The Main Idea

I initially thought pointers were going to be one of the most complicated parts of learning Go.

They're definitely more visible than they are in Ruby, but the basic idea is pretty simple:

& → give me the address
* → give me the value at that address
Enter fullscreen mode Exit fullscreen mode

And when working with structs:

person := &Person{}
Enter fullscreen mode Exit fullscreen mode

means:

person points to a Person.

While:

func (p *Person)
Enter fullscreen mode Exit fullscreen mode

means:

this method works with a pointer to the original Person.

As a Ruby developer, I probably won't need to think about memory addresses every day.

But understanding pointers gives me a better picture of what's actually happening when Go passes values around.

And that's becoming one of my favorite parts of learning Go:

Ruby lets me stay comfortably high-level.

Go occasionally makes me look under the hood.

Top comments (0)