Channels inside channels pattern in Golang

Gergő Huszty
Level Up Coding
Published in
5 min readFeb 4, 2020

--

Photo by chuttersnap on Unsplash

As part of my daily job working on IBM Cloud, sometimes I write code in Golang. This article is about a technique that I found useful.

Channels in Golang are great. They are expected to solve various problems when it comes to communication inside the process. There are a lot of tutorials that show solutions based on channels + go-routines + contexts. Patterns include fan-in, fan-out, pipelines, rate limiters, load-balancers, etc. However when I started to create code from scratch, sometimes I struggled, especially with request-response cases. It appeared that I should still use mutexes, but I didn’t want to. This post is about how to not do that and put channels inside channels instead.

The problem

It sounds odd first, so let me show a real-life example. Imagine you have a piece of data, changing from time to time. You might assign an owner method running in a go-routine to conduct the updates. Say your process is a network service and the subject data is served via its API when a client request comes. That means there will be data reads from the same data structure, which may change inside its owner go-routine meanwhile. Let’s see a dummy example of this:

As you can see, the Run() function is changing the data every second. The Get() function of the same structure is able to read the data whenever called. After that, there is a test case without any assertions, but it is still useful. It spawns the Run() function, sleeps a second and does a data read. There is a very useful tool in the Golang toolchain, which is able to detect data-races. Try it with this command: go test . -race. The result is kind of expected, we have a serious data race:

➜ go test . -race
==================
WARNING: DATA RACE
Write at 0x00c00009e070 by goroutine 9:
github.com/libesz/datarace.(*Data).Run()
/Users/gergo/go/src/github.com/libesz/datarace/datarace_test.go:22 +0x213
Previous read at 0x00c00009e070 by goroutine 8:
github.com/libesz/datarace.TestConcurrent()
/Users/gergo/go/src/github.com/libesz/datarace/datarace_test.go:29 +0x96
testing.tRunner()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:909 +0x199
Goroutine 9 (running) created at:
github.com/libesz/datarace.TestConcurrent()
/Users/gergo/go/src/github.com/libesz/datarace/datarace_test.go:34 +0x7a
testing.tRunner()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:909 +0x199
Goroutine 8 (running) created at:
testing.(*T).Run()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:960 +0x651
testing.runTests.func1()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:1202 +0xa6
testing.tRunner()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:909 +0x199
testing.runTests()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:1200 +0x521
testing.(*M).Run()
/usr/local/Cellar/go/1.13.4/libexec/src/testing/testing.go:1117 +0x2ff
main.main()
_testmain.go:44 +0x223
==================
--- FAIL: TestConcurrent (1.00s)
FAIL
FAIL github.com/libesz/datarace 1.325s
FAIL

It perfectly reports that the read and the write may happen at the same moment and can cause data corruption. The problem is really that there can be multiple concurrent tasks in your application which want to access the same data. You never know how the data write in your owner go-routine and the reads will be scheduled into operating system threads. This is the classic multi-writer multi-reader use-case and this is where usually the solutions are going into locking algorithms (how to use rw-mutexes, etc.). Even Golang tutorials are suggesting mutexes here, while the language philosophy is: Do not communicate by sharing memory; instead, share memory by communicating.

Some initial attempts

So we want to create a generic pattern with no mutex or other sync primitive usage in the code for the use-case above. Channels should always help, right? :)

But in order to send anything through a channel, the handler for it must be known by both the sender and the receiver. Should it be part of the Data structure? Say we have an UpdateChan chan int field in the structure where the owner go-routine can announce the changes. We instantly have multiple problems:

  • If no one is actually interested in the update, the sender part will be blocked. If we create a buffered channel, then the outdated updates will be consumed first.
  • If the request is ad-hoc (i.e. when someone does an API request), the information push to the channel shall be also done on-demand.

Could we use a channel to both send the data read request, and then send back the data on the same? Well, better to not. Channels are typed and this is a great feature of Go. That means the data shall have a concrete data type and it is better to avoid struct{} plus nasty casts. That also implies that the sender and the receiver shall not be swapped any time to do bi-directional communication.

Seems we need two channels to fix this… One to send the read request and another to send back the data to the requester go-routine. Something like this:

type Data struct {
secretOfTheSecond int
ReadRequest chan struct{}
ReadResponse chan int
}

We are getting closer. If any go-routine wants the data, it sends anything into the ReadRequest channel and starts listening on the ReadResponse channel. Run() waits for anything pushed into the ReadRequest channel and pushes the current data to the ReadResponse channel.

But we now have a new problem…

A channel can be listened to freely by multiple go-routines at the same time. If more than one receiver is interested in the update coming out from the response channel, only one of them will be able to read an update. Of course, if there are multiple receivers, they all should send data requests, but which response is for whom, etc.. As we feel, this is not bulletproof.

The response channel shall be owned by the data requester and shall not be shared.

Invent channel in channel

Of course I did not invent this. Channels are first-class citizens, a kind of basic data type in Go. As part of the declaration, they get another arbitrary data type assigned, which they can transport later on. This being said, a channel can hold any data type, which again, can be a channel! Elegant.

Our new data structure can look like this:

type Data struct {
secretOfTheSecond int
readRequest chan chan int
}

Now we can re-implement the Get() method for the struct to create an exclusive channel (of type int) in the reader’s context and send it into the readRequest channel. The data owner go-routine can dispatch this event and can extract the channel created by the data requester to send the response.

The complete example looks like this:

As we can see, we don’t have data race anymore:

➜ go test . -race
ok github.com/libesz/datarace 2.200s

As part of the enhancement, we started to change our Run() method into a small event loop (error handling is not implemented for clarity). It will serialize all the incoming requests with the data updates. You can extend this example by adding multiple different read (or write!) requests, with their respective channel in channel member fields in the struct.

I found this pattern very clean and elegant. You can introduce it for multiple use-cases. Of course, under the hood channels are using lower level synchronization primitives, like mutexes and shared memory, but we don’t necessarily have to get our own hands dirty with those :).

If you have a very performance and timing sensitive application, you might want to read another article before applying the idea above. It is a detailed benchmark about Golang channel performance: https://syslog.ravelin.com/so-just-how-fast-are-channels-anyway-4c156a407e45

--

--