Should We Use Pointers In Go? Truth Is Complicated.

Mert Simsek
Beyn Technology
Published in
7 min readSep 30, 2021

In this post, I’d like to cover pointers and reference/dereference techniques briefly. Actually, pointers do allow us to save memory when passing values. This is true. In this case, we should always get more memory and performance, right? Truth is complicated a bit. Keep garbage collector in mind, we’ll face it after covering pointers. Let’s dive into the deep.

Pointers are the addresses of variables. So, the pointer is the location where the value is stored. With a pointer, we can indirectly read or update the value of a variable without using its name or even knowing its name. We need to know the * and & operators, which are called pointers in Golang. The & operator is used to find the address of the variable, while the * operator takes the value at the address pointed by the pointer. Now let’s reinforce what we said with an example.

package main

import (
"fmt"
)

func main(){
s := "my-text-message"
fmt.Println(s)

i := 123
fmt.Println(i)
}

We set two variables normally and print them out, no strange way that we don’t know. However, If we send a variable to a function, then Go creates a replica of that parameter and uses it inside the function like this.

package main

import (
"fmt"
)

func customPrint(s string){
fmt.Println(s)
}
func main() {
s := "this-is-my-text-message"
customPrint(s)
}

customPrint function gets a string which it will assign to the variable of s and then print it on the screen, Go creates a replica of the value that was originally sent and use it inside the function and when it is finished, the garbage collector takes those variables and erases of them. Right, let’s do it the same way, by a pointer.

package main

import (
"fmt"
)

func customPrint(s *string){
fmt.Println(s, *s)
}
func main() {
myOtherString := "this-is-my-text-message"
customPrint(&myOtherString)
}

For this time we got some strange characters and our text message. In the main function, &(ampersand) tells Go that the value is going to be a pointer, Go looks at that value, handle the data type and handle where that is located in memory, once it does have that address it sends the address in memory to the function.

In customPrint the (*)star/asterisk, tells Go we will get a pointer with the data type of a string, it is very significant to keep in mind that pointers need a type, pointer to an int, a pointer to a string, a pointer to a slice, etc. Once that the data type for the parameter has been set, we print it in the function body a couple of times, first, we use “s” as a pointer to a string, and then we add a start before it, that tell Go to take that pointer, then get the actual value that is stored in that memory address.

Struct

package main

import (
"fmt"
)

type Player struct {
Name string
}

func main() {
p := Player{Name: "Mert"}
customPrint(&p)
}

func customPrint(p *Player){
fmt.Println(p, *p, p.Name, (p).Name)
}

Firstly we receive the structure value printed but prefixed with an ampersand, meaning that it’s a pointer to a structure with those contents When we use the star, we’re telling it to get the value so we get {Mert} which is the formatted structure value. Once you get a field of a pointer to a structure, Go will take care of the dereferencing for you, and you will get the value as if it was a structure and not a pointer to one. Then the final usage is surrounding the structure inside a parenthesis, this is to specify the dereference, which is unnecessary in this case since Go handles it as we saw above.

If we’d like to update a variable inside a function.

package main

import (
"fmt"
)

type Player struct {
Name string
}

func main() {
l := []string{"İstanbul","Ankara", "İzmir"}
customAppend(l)
fmt.Println(l)
}

func customAppend(l []string){
fmt.Println(l)
l = append(l, "Bursa")
}

If we don’t use pointers, Go will copy that and the function will handle this copied variable. Let’s use the pointers in this example.

package main

import (
"fmt"
)

type Player struct {
Name string
}

func main() {
l := []string{"İstanbul","Ankara", "İzmir"}
customAppend(&l)
fmt.Println(l)
}

func customAppend(l *[]string){
fmt.Println(*l)
*l = append(*l, "Bursa")
}

As you see, our new variable is appended successfully. OK, this was expected whereas what happens when we try to do maps?

Maps will be updated inside the function and that became the actual value of the original map, not just the copy, this is because maps are truly pointers, so we really don’t need to do anything with maps.

This brings me to the end of what I was going to say about them. So should we use pointers always because they really help to save memory a lot? Let’s dive into the deep a bit.

Go’s Garbage Collector

When you send pointers to functions, and the scope of that function ends, the garbage collector doesn’t know if it is supposed to erase that variable or not, and then we allow the original data to stay more than we would have liked because of it.

https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-watch-your-application-dbef99be2c35

Does that affect us so much that pointers are useless? No, but the amount of time the garbage collector takes to erase that will be a bit longer but in most cases, it will still be tolerated, if you are looking at squeezing every last bit of performance, then you will have a possible reason to try and optimize your pointer usage.

There is an awesome example in the following post regarding this issue.

According to it, we are supposed to avoid pointers in some cases. In large heaps, pointers are evil and must be avoided. But you need to be able to spot them to avoid them, and they aren’t always obvious. Strings, slices, and time. Time all contain pointers. If you store a lot of these in-memory it may be necessary to take some steps.

When I’ve had issues with large heaps the major causes have been the following.

  • Lots of strings
  • Timestamps on objects using time.Time
  • Maps with slice values
  • Maps with string keys

There’s a lot to say about different strategies to deal with each of these. In this post, I’ll just talk about one idea for dealing with strings. I’d like to leave an example from that article.

func main() {
a := make([]*int, 1e9)

for i := 0; i < 10; i++ {
start := time.Now()
runtime.GC()
fmt.Printf("GC took %s\n", time.Since(start))
}

runtime.KeepAlive(a)
}

The garbage collector takes over half a second. And why should that be surprising? They allocated 1 billion pointers. That’s actually less than a nano-second per pointer to check each pointer. Which is a pretty good speed for looking at pointers.

In the example, we’re allocating exactly the same amount of memory as before, but now our allocation has no pointer types in it. We allocate a slice of a billion 8-byte ints, again this is approximately 8GB of memory.

func main() {
a := make([]int, 1e9)

for i := 0; i < 10; i++ {
start := time.Now()
runtime.GC()
fmt.Printf("GC took %s\n", time.Since(start))
}

runtime.KeepAlive(a)
}

The garbage collector is considerably more than 1000 times faster, for exactly the same amount of memory allocated without pointers.

To Sum Up

Pointers are not inefficient. I just tried to say if we have large amounts of memory allocated on-heap, and when we try to work around this by moving the data to our own off-heap allocation, you will come across problems in your Go applications. If we can avoid any pointers within the types we’re allocating they won’t cause garbage collector overhead. Strings, slices, and time. Time all contain pointers. If you store a lot of these in-memory it may be necessary to take some steps. Perhaps, we could cover this topic in the next article.

--

--

Mert Simsek
Beyn Technology

I’m a software developer who wants to learn more. First of all, I’m interested in building, testing, and deploying automatically and autonomously.