Small Lesson About Pointers & Slices in Go

Today I learned a little bit more about how pointers work in Go.

We had this object that we want other functions to directly modify. So we pass a pointer to this object to various functions and then in our tests we check to see that the original object has been modified correctly. We also have these helper functions that are supposed to return a pointer to an object buried inside of the struct.

Here's an example of our object:

type obj struct {
    num   int
    str   string
    slice []int
}

And then we had a helper function that is supposed to return a reference to an element inside of the slice.

Working with our example object, I had a helper function that looked like this:

func FindNumber(object *obj, findInt int) *int {
    for _, singleInt := range object.slice {
        if singleInt == findInt {
            return &singleInt
        }
    }
    return nil
}

It's supposed to go into a given object and return a pointer to the matching element inside the obj.slice parameter. If we put this together right now we get this program:

package main

import "fmt"

type obj struct {
    num   int
    str   string
    slice []int
}

func main() {
    myObject := obj{num: 1, str: "hello", slice: []int{1, 2, 3}}

    pointerToNumber := FindNumber(&myObject, 2)

    *pointerToNumber = 5 // change the value from 2 to 5
    fmt.Printf("%p - %v - from FindObject()\n", pointerToNumber, *pointerToNumber)
    fmt.Printf("%p - %v - Pointer address I actually wanted\n", &myObject.slice[1], myObject.slice[1])
}

func FindNumber(object *obj, findInt int) *int {
    for _, singleInt := range object.slice {
        if singleInt == findInt {
            return &singleInt
        }
    }
    return nil
}

And when we run it we get this output:

0xc00001a0b8 - 5 - from FindObject()
0xc0000182e8 - 2 - Pointer address I actually wanted

The value didn't change to 5 in my original object and we can see the pointer address doesn't match. Any changes I make to the value of this pointer won't be reflected in the myObject variable.

Let's think about why this is happening, going line by line.

First we make our object:

myObject := obj{num: 1, str: "hello", slice: []int{1, 2, 3}}

and we pass a pointer of our object to FindNumber(), telling it to return a pointer to the element within the slice that equals 2:

pointerToNumber := FindNumber(&myObject, 2)

Inside FindNumber() we we get the slice from the pointer and iterate over it:

for _, singleInt := range object.slice {
    ...
}

Let's stop and think about this for loop. We have a pointer to object and we're using that pointer to access the slice parameter. Let's keep in mind that a slice is just a pointer to an array and not a pointer to each element contained in the array.

So when range gets an element from whatever slice it's been passed, it passes the value of that element to our singleInt variable. So singleInt is not a pointer to the element, it's just a copy of the original element! If I want to return a pointer to the actual element in the slice I'll have to use the index returned from range and return the pointer from the original slice:

func FindNumber(object *obj, findInt int) *int {
    for index, singleInt := range object.slice {
        if singleInt == findInt {
            return &object.slice[index]
        }
    }
    return nil
}

Now my output is this:

0xc0000182e8 - 5 - from FindObject()
0xc0000182e8 - 5 - Pointer address I actually wanted

The addresses match and the value changed!

There's also another way to solve this problem that shows some of the dangers with pass-by-value in Go.

If we change FindNumber() to not accept a pointer:

func FindNumber(object obj, findInt int) *int {
    fmt.Printf("%p - address of object in FindNumber\n", &object)
    ...
}

And not pass in a pointer to our object:

fmt.Printf("%p - address of myObject\n", &myObject)
pointerToNumber := FindNumber(myObject, 2)

and run our code now:

0xc000076180 - address of myObject
0xc0000761b0 - address of object in FindNumber
0xc000090028 - 5 - from FindObject()
0xc000090028 - 5 - Pointer address I actually wanted

We see that FindNumber() got a copy of the original object but the pointer it returned still allowed us to change the value in the original object. So when we pass-by-value any struct, the slices inside the struct point to the same array. And this makes sense because a pointer is just a memory address and copying an address doesn't change the location of the address. If a pointer says go to 0xc000076180, when I copy that, my new pointer will also say go to 0xc000076180.

If you want to avoid accidentally modifying the original slice then you should create a copy() of the slice.

Hope this taught you a little more about slices and pointers in Go.

Posted by Taylor