Pointers in Golang

Rene Manqueros
Analytics Vidhya
Published in
7 min readMay 30, 2021

Pointers are a very basic but extremely powerful tool that are mostly misunderstood and considered as a difficult or frustrating concept when programming. Pointers were created to help pass data around the code without having to create copies of the variables, thus not using extra memory.

Some languages avoid the concept, others partially embrace it but in Go we have pointers as they were meant to be: ̶a̶n̶ ̶i̶n̶s̶t̶r̶u̶m̶e̶n̶t̶ ̶o̶f̶ ̶t̶o̶r̶t̶u̶r̶̶e a great tool to optimize performance.

A pointer is a variable that references a certain location in memory where a value is stored, so you take a variable, place it on memory then instead of reading the actual value, you just get the address of where that value is; When you pass it around the code, since it’s just a “number” it is cheaper to move than the actual value, which could be way longer than just the address (this will be important at the end of the article).

Let’s get started with some code:

package main

import (
"fmt"
)

func main(){
myString := "a string value"
fmt.Println(myString)

myInteger := 1
fmt.Println(myInteger)
}

We define two variables and print them out, no weird behavior yet

no pointers for now

If we send a variable to a function, then the language will create a copy of that parameter and use it inside the function like so:

func myFunction(s string){
fmt.Println(s)
}
func main() {
myOtherString := "another message here"
myFunction(myOtherString)
}

When you do this, myFunction, receives a string which it will assign to the variable of “s” and then print it on the screen, Go (and most languages) will create a copy of the value that was originally sent and use it inside the function, once the function reaches the end, the garbage collector will take those variables and dispose of them.

Now let us try to achieve the same behavior, using a pointer:

func myFunctionWithPointers(s *string){
fmt.Println(s, *s)
}
func main() {
myOtherString := "another message here"
myFunctionWithPointers(&myOtherString)
}

This time we have some weird characters decorating our variables:

Inside main(): the ampersand ( & ), will tell Go that the variable is going to be a pointer to that variable, Go will look at that variable, figure out the data type and figure out where that is located in memory, once it has that address it will send the address in memory to the function.

On myFunctionWithPointers: the star/asterisk ( * ), will tell Go: we are going to receive a pointer with the data type of a string, it is very important to note that pointers need a type, pointer to an int, pointer to a string, pointer to a slice, etc. Once that the data type for the parameter has been defined, we’ll print it in the function body a couple of times, first we will treat “s” as a pointer to a string, and then we’ll add a start before it, that will tell Go to take that pointer, then fetch the actual value that is stored in that location:

0xc000010200 is the memory address, your’s will be different

The first time we used the variable “s” it was the pointer so we get the address, the second time we “dereference” it, which is taking the address then taking the value in that location.

Let us try working with a structure now:

type myStructure struct {
Name string
}

func main() {
structure := myStructure{Name: "MyName"}
structureFunction(&structure)
}

func structureFunction(e *myStructure){
fmt.Println(e, *e, e.Name, (e).Name)
}

This prints out the following:

First we get 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 {MyName} which is the formatted structure value.

When you access 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 redundant in this case since Go handles it as we saw above.

If we want to update a variable inside a function:

func main() {
sliceValues := []string{"a","b", "c"}
appendToSlice(sliceValues)
fmt.Println(sliceValues)
}

func appendToSlice(c []string){
fmt.Println(c)
c = append(c, "d")
}

Not using pointers will end up not updating the variable that is outside of the function context.

Lets add pointers everywhere now:

func main() {
sliceValues := []string{"a","b", "c"}
appendToSliceWithPointer(&sliceValues)
fmt.Println(sliceValues)
}

func appendToSliceWithPointer(c *[]string){
fmt.Println(*c)
*c = append(*c, "d")
}

When invoking the function, this time we’ll use the pointer notation, as well as when receiving the parameter inside the function, when we print the value, we will tell it to deference it so that we get the actual values of the slice and no the memory location, and the part that might look confusing is when we append to our slice, since append expects a slice, we’ll have to dereference “c”, add the value of “d” and then the return value of append, will have to be stored as the value of “c” and not the pointer (we would be trying to store a slice where a memory location is expected).

This time the manipulation outlived the function scope

OK this seems easy, but what happens when we try to do maps

func main() {
myMap := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
maps(myMap)
fmt.Println(myMap)
}

func maps(c map[string]int){
c["d"] = 4
fmt.Println(c)
}

In this example, the change to the map behaved different, it was updated inside the function and that became the actual value of the original map, not just the copy, this is because maps are really pointers, so we really don’t need to do anything with maps (one less thing to worry about).

And guess what, functions are also pointers:

func main() {
f := &myFunc
}

func myFunc(){

}

So that won’t work, since myFunc is already a pointer (2 down, a few more to go…)

Since the pointer is practically a data type, function receivers will get a similar behavior:

type myStructure struct {
Name string
}

func (ms myStructure) noPointer(){
ms.Name = "xxxx"
fmt.Println(ms.Name)
}

func (ms *myStructure) withPointer(){
ms.Name = "yyyy"
fmt.Println(ms.Name)
}

func main() {
theStructure := myStructure{Name: "MyName"}
theStructure.noPointer()
fmt.Println(theStructure.Name)

theStructure.withPointer()
fmt.Println(theStructure.Name)
}

When the receiver is used as structure, the value is not affected outside that function.

When the receiver is handled as a pointer, the value will behave as we now expect.

But, if you try to call the method directly:

func (ms myStructure) noPointer(){
fmt.Println("Not a pointer")
}

func (ms *myStructure) withPointer(){
fmt.Println("a pointer")
}

func main() {
myStructure{}.noPointer()
myStructure{}.withPointer()
}

That second function “withPointer” will just not run, “withPointer” is a receiver linked to the “pointer to myStructure” data type and myStructure{} is an instance of myStructure; We can fix this by instancing the structure and then adding a pointer to it:

type myStructure struct {
Name string
}

func (ms myStructure) noPointer(){
fmt.Println("Not a pointer")
}

func (ms *myStructure) withPointer(){
fmt.Println("a pointer")
}

func main() {
myStructure{}.noPointer()
ms := &myStructure{}
ms.withPointer()
}
Much better

And that’s pretty much all you need to get you started with pointers :)

Which leads us to the questions: So when do I use pointers? Always?

Somehow this has become my favorite meme

A pointer will allow us to save some memory when passing values right, since that sounds optimal, then we should it always so we are always saving memory.

Unless… cue the villain from the start of the article that no one remembers…

…. Go’s garbage collector walks in…

When you pass pointers to functions, and the scope of that function ends, the garbage collector doesn’t know if it should kill that variable or not (give this article a look for a more detailed explanation: https://blog.gopheracademy.com/advent-2018/avoid-gc-overhead-large-heaps/), and then we end up with the original data living more than we would have like because of it.

Would that affect us so much that pointers are meaningless? not really, the amount of time the GC will take to remove that will be a bit longer but in most cases it will still be manageable / unnoticeable, 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, otherwise it falls under the badly-applied-micro-optimizations bucket.

--

--