When Should You Use Pointers in Go

When Should You Use Pointers in Go

And why you should use them less than you think.

ยท

6 min read

The concept of pointers in programming languages is quite old. C and C++ are probably the best examples of languages that are widely popular that use pointers. Golang, being inspired by C in many ways, implements explicit pointers as well. I say explicit, because the most popular programming languages like Python and JavaScript implement pointers also, except they do them implicitly so you don't even know about this.

Ever hear of the term "pass by reference" ? It's used all the time in Python, JS and many more. Here's an example in Python to describe what I mean:

a = [1,2,3]
b = a
b.append(4)
print(a)

You'll see from this simple example that not only are the values of a and b the same, but a and b are the same. You can check this by printing the memory addresses of both:

print(id(a))
print(id(b))

The reason they are the same is because when we say b=a, we're not saying assign b the value of a, rather assign b to be a pointer to a.

Languages like Python and JavaScript were created to abstract away many low level concepts so that developers had less on their mind, and could focus more on business logic. These languages were created in the 1990s when memory was scarce. Why is that important for pointers? Well, imagine you have a data structure like this:

people = {1: {'name': 'John', 'age': '27', 'sex': 'Male'},
          2: {'name': 'Annie', 'age': '22', 'sex': 'Female'},
          3: {'name': 'Jane', 'age': '42', 'sex': 'Female'},
          4: {'name': 'Bob', 'age': '31', 'sex': 'Male'},
          5: {'name': 'Andy', 'age': '36', 'sex': 'Male'},
          6: {'name': 'Lisa', 'age': '32', 'sex': 'Female'},
          7: {'name': 'Jimmy', 'age': '26', 'sex': 'Male'},
          8: {'name': 'Gregory', 'age': '36', 'sex': 'Male'}}

Let's see how much memory this object will take up when placed in the heap:

print(sys.getsizeof(people))

360 bytes. Not too big, but we could easily imagine this dictionary containing 800 keys instead of 8. Imagine copying the value of this variable whenever we wanted to do something with it. You would potentially run out of memory if memory was scarce and expensive, like in the previous decades. Wouldn't it be cheaper to just copy a reference to this variable. It's like having 200 pounds of sugar (for whatever reason maybe you really like sugar, just stick with me) . Would you rather always carry that huge load of sugar around with you, or would you perhaps store it in some locker, write down the address of that locker, and then when you need some (or all) of that sugar you just go to that address.

That's the whole point of pointers (no pun intended).

Pointers are integers that store the value of the memory address. Being integers, they only take up 4 bytes on a 32 bit system, or 8 bytes on a 64 bit system. Which is way less than the 360 bytes (or potentially 36000 bytes) that our people variable value takes up.

However, passing around references can get complex and error prone sometimes, that is why it is good to have an option to pass by value as well.

Golang, unlike Python or JavaScript, is a pass by value language. If you want to pass by reference, you use explicit pointers.

Okay, so basically we should use pointers all the time, right?

No.

In fact, I'll quote Ben Darnell, who is the lead engineer of CockroachDB (built in Go) :

"Whenever in doubt, use a value instead of a pointer"

We're no longer in the 1990s , memory is cheap. If you don't need a pointer, you should stick to a value. However, in many cases, you will indeed need a pointer.

Here are some of the common situations where you will need to use a pointer instead of a value:

1. Changing the state of an object:

Let's say you have a struct like this

type Person struct {
    name     string
    age      int
    gender   string

}

Now, you want a method that will enable you to change the name of a person. Coming from Python, your gut instinct would tell you to write

func (p Person) changeName(name string){
    p.name = name
}

The problem is, Golang being Golang, you are passing by value, meaning you are not passing the real p but only a clone that copies it's values. Changing the state of the clone will not affect the original.

This is why you need a pointer here.

func (p *Person) changeName(name string){
    p.name = name
}

2. Enabling variables to have nil values

This is a gotcha that leads a lot of Go beginners to create bugs. A variable that is defined, but not assigned a value, does not evaluate to nil but rather to it's default value. For booleans this is false, for integers it is 0, for strings it is an empty string. But, what if you need it to be nil. Say you have a REST API endpoint that allows end users to create accounts in your application by using the POST /accounts endpoint. They pass their info in the body of this request and your backend Go server then converts this JSON body into a User struct, does some validations, and then stores this data in the database. Suppose you write your struct like this:

type User struct {
    username   string
    email      string
    profession string
    country    string
}

Now, let's say that some user sends their POST request to your server, but leaves the profession parameter out of the body because it is a non-required parameter, and the user wishes not to specify their profession. The way this struct is defined, the profession would not evaluate to nil`` , but to''``` . Is this some kind of new profession? Or is this intended to be empty? Did the user mistakenly pass an empty string? Here, it would be more appropriate to use a pointer to a string, instead of just a string. You could also imagine situation where an integer evaluates to 0, when the value shouldn't even exist, because it wasn't specified. Imagine a football match struct:

type Match struct {
    homeTeam        string
    awayTeam         string
    HomeTeamGoals int
    AwayTeamGoals    int
}

What if the match hasn't started yet. Wouldn't it be more appropriate if the HomeTeamGoals and AwayTeamGoals were nil instead of evaluating to 0.

Your application logic would be able to assert if a value is nil, then the match hasn't started. So better to define the struct like this:

type Match struct {
    homeTeam   string
    awayTeam      string
    HomeTeamGoals *int
    AwayTeamGoals    *int
}

Make it a rule that if you need a variable to be able to evaluate to nil, you need a pointer.

3. Copying struct that have very large values:

Although memory is not that big of an issue today, you might still want to use a reference if the value of the object is ridiculously large, and there is really no need to pass it around all the time. This will probably improve the performance and memory usage of your application (although probably not significantly) . There is a trade off, because now the garbage collector has more work to do, but in cases of very large and complex structs, I think using pointers makes more sense.

As you can see, using pointers is sometimes over-engineering, and although it makes the code seemingly look sophisticated it's actually doing damage. However, sometimes you have no other option.

Thanks for reading, and if I missed anything important, please leave a comment :)

Did you find this article valuable?

Support Pavle Djuric by becoming a sponsor. Any amount is appreciated!

ย