Go's Channels and Pointers

Background

I've used Java for most of my career and am used to the fact all objects are passed by reference, while all primitive types are passed by value. Java has no pointers and you don't have to think about how you pass things around. In Go, structs are passed by value, so the runtime copies its fields when you pass the struct to a function, for example. However, Go also supports pointers and you can pass a pointer to the struct instead to allow the function to read and change the original struct's fields.

Let's now look at channels, which is another Go feature that many other languages miss. Channels are like queues supported by the language itself, we can write to them and read from them asynchronously, that is without blocking the writer or the reader. Channels are often used to allow for concurrent processing of data.

Now, should you be putting struct values or pointers on your channels? In this post, I will argue that you should be using channels with values rather than pointers. I should acknowledge that there are likely exceptions that I am not aware of but they should not be common.

A Straightforward Example

Let's look at this example taken from this StackOverflow answer. It creates a channel of pointers to int values and then writes pointers to int values from 0 to 9 to it. At the same time, another thread reads values from the channel and prints them out.

Go Playground Link.

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan *int, 1)

    go func() {
        val := new(int)
        for i := 0; i < 10; i++ {
            *val = i
            c <- val
        }
        close(c)
    }()

    for val := range c {
        time.Sleep(time.Millisecond * 1)
        fmt.Println(*val)
    }
}

Here is the output:

2
3
4
5
6
7
8
9
9
9

It is quite easy to see what is wrong with the code. It writes the same pointer to the channel every time. So the consumer gets whatever the int value happens to be in that memory address at the time it is being read.

Let's try to fix it by writing a pointer to a new value every time.

An Example That Works

This example creates an array and writes a reference to each array value in a loop. Go Playground Link.

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan *int, 1)

    go func() {

        vals := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        for i := 0; i < 10; i++ {
            c <- &vals[i]
        }
        close(c)
    }()

    for val := range c {
        time.Sleep(time.Millisecond * 1)
        fmt.Println(*val)
    }
}

Here is the output:

1
2
3
4
5
6
7
8
9
10

So it looks like this works as expected. Let's now use the range loop instead to make sure it works too.

An Example That Is Confusing

Go Playground Link.

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan *int, 1)

    go func() {

        vals := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        for _, val := range vals {
            c <- &val
        }
        close(c)
    }()

    for val := range c {
        time.Sleep(time.Millisecond * 1)
        fmt.Println(*val)
    }
}

It looks like this should work as well but it does not.

3
4
5
6
7
8
9
10
10
10

Why does it fail? Because of this Go feature:

It is not possible to create a Go program where two variables share the same storage location in memory. It is possible to create two variables whose contents point to the same storage location, but that is not the same thing as two variables who share the same storage location.

So the last example again writes the same address to the channel every time.

Conclusion

It is very easy to make mistakes when writing and reading pointers from channels. You should probably avoid using pointers in this scenario. Your teammates will thank you.

You may think that pointers will save you some memory and processing time but that is not necessarily the case. Go is optimized for passing objects by value and copying is fast. As always, do some profiling before you try to do any kind of optimization.