Neat snippet for y’all today. I’m calling this snippet “channel smuggling”; I googled this and all that came up was weird stuff. Anyway, the idea here is that suppose you want to pass a piece of data into a channel and have something do some work on it. However, what if you want to block until after all the work on that data is completed? Here’s an example on Go Playground, and here it is replicated below:

package main

import (
	"fmt"
	"time"
)

var dest string

type worker struct {
	ingress chan string
}

func (w *worker) slow(s string) {
	time.Sleep(3 * time.Second)
	dest = s
}

func (w *worker) Send(s string) {
	w.ingress <- s
}

// Run runs in its own goroutine pulling items off the channel
func (w *worker) Run() {
	for s := range w.ingress {
		dest = s
	}
}

func main() {

	// run the worker in its own goroutine
	p := worker{ingress: make(chan string)}
	go p.Run()

	// We want this function to block so that we know the whatever
	// "work" it initiates is completed before continuing.
	block := func(s string) {
		p.Send(s)
	}
	block("foo")

	fmt.Printf("dest contains: %s", dest)
}

Can you spot the bug? The block function doesn’t actually block, so what prints is the zero value of this dest variable (don’t judge the fact that I’m mutating this global variable, it’s simply for demo purposes, maybe in practice it represents some entry in a database far far away).

Ok, so what’s the issue here? You might think “the worker is using an unbuffered channel, so Send should block”, and you’d be kind of right. In fact, sometimes you might even get the right result (after all, this is a race condition). The call to Send will indeed block, but only until the data is read from the channel. I’ve given you a hint: in Run after reading the data from the channel, we call slow. Depending on how slow slow is, execution might get handed back to the main goroutine. Once the data is read from channel, Send is unblocked, and suddenly we’re in trouble.

The question then becomes: how do we get Send to block? Channel smuggling!!! Maybe there’s an actual name for this, I don’t know. I’m almost certain I’ve seen this somewhere before and just happened to re-arrive at it during my own toils (which is always fun). In short, we’re going to refactor the channel so that we can send the data and smuggle a done channel along with it. That’s right, channels in channels (yo dawg). We can block on receiving from the done channel in Send (which makes block actually block). Once the work in slow is finished, we can send on the done channel, unblocking Send, and we’ll be golden! It’s worth noting you can do something very similar with a context.Context and context.WithCancel, but I didn’t opt for that here since this is a little more obvious, if less idiomatic. Here it is on Go Playground, and here it is replicated below:

package main

import (
	"fmt"
	"time"
)

var dest string

type req struct {
	data string
	done chan struct{}
}

type worker struct {
	ingress chan req
}

func (w *worker) slow(s string) {
	time.Sleep(3 * time.Second)
	dest = s
}

func (w *worker) Send(s string) {
	done := make(chan struct{})
	w.ingress <- req{data: s, done: done}
	<-done
}

// Run runs in its own goroutine reading items from the channel
func (w *worker) Run() {
	for r := range w.ingress {
		w.slow(r.data)
		r.done <- struct{}{}
	}
}

func main() {

	// run the worker in its own goroutine
	p := worker{ingress: make(chan req)}
	go p.Run()

	// we want this function to block so that we know the whatever
	// "work" it initiates is completed before continuing
	block := func(s string) {
		p.Send(s)
	}
	block("foo")

	fmt.Printf("dest contains: %s", dest)
}

That’s all there is to it! I hope this helps!!