Cytora Golang Academy: Part Two

Adelina Simion
Engineering at Cytora
7 min readFeb 9, 2021

--

The purpose of the Cytora Golang Academy blog post series is to share the Goroutines & Channels talks series that I have been working on at Cytora.

Everyone has thoroughly enjoyed preparing and listening to this series and I hope you will enjoy the magic show themed Cytora Golang Academy series just as much!

This is the second blog post in the series. You can find part one here ,if you missed it.

Apprentice to Illusionist: Channels deep dive

My second session in the Goroutines & Channels talk series explores the behaviour of Go channels in depth.

Sending & receiving on channels

As channels were only introduced at the end of the first session, it is important to revisit the behaviour of sending & receiving data on them… with a brand new trick!

The “Teleporting Coin” trick starts up two Goroutines: an assistant and a magician. They quickly pass & reveal a coin between each other, delighting the audience with their lightning speed manoeuvres! ⚡️⚡️

The coin performers use a channel to pass the coin for a predetermined amount of times. At the comma, the CoinPerfomer Goroutine writes its name to a separate channel once it has finished passing the coin around. Easy Peasy!

type CoinPerformer struct {
name string
passes int
}
func (c *CoinPerformer) perform(coin chan bool, done chan string) {
fmt.Printf("%s ready to start the teleportation trick.\n", c.name)

for i := 0; i < c.passes; i ++ {
// receive coin from other performer, blocking
<- coin

fmt.Printf("%s has the coin!\n", c.name)
time.Sleep(2 * time.Second)

// send the coin to the other performer, blocking
coin <- true
}

//the performer has finished and is ready to tell main they are done, blocking
done <- c.name
}

This trick shows how channels can be used to pass information between Goroutines and demonstrates the difference between an unbuffered channel and channel with capacity 1. While it might be tempting to think of these two as equivalent, the trick will demonstrate that their behaviour is quite different!

First, the coin performers use an unbuffered channel to pass the coin around, declared simply by

coin := make(chan bool)

The trick fails with the Go runtime declaring a Goroutine deadlock! 💥

The reason for the failure is due to the nature of channels. Unbuffered channels need both sender and receiver to be present to make the exchange. The performer that first reaches their limit takes a bow and exits. The other performer then tries to send the coin to the coin channel, which then blocks forever as no one will ever try to read from the channel.

Changing the trick and allowing the coin performers to use a buffered channel with capacity 1, as below, solves this deadlocking issue.

coin := make(chan bool, 1) 

The performer that first reaches their limit takes a bow and exits. The other performer then tries to send the coin to the coin channel. The buffered coin channel keeps the coin at the end, even if the other magician finishes and takes a bow. The trick can now succeed! 🥳

Closing channels

While we’ve only discussed sending and receiving to channels so far, closing channels is an important operation which allows Goroutines to signal no more values will arrive on the channel.

The syntax is very simple:

close(coin)
  • A closed channel will immediately return the zero value of that type when read from.
  • Receivers can use an additional variable while receiving data from the channel to check whether the channel has been closed.
c, ok := <- coins
if ok {
fmt.Printf("%s has been teleported!", c)
}
  • If ok is true, then the value was received by a successful send operation to a channel. If ok is false, then we are reading from a closed channel.
  • Alternatively, receivers can use the for range form of the for loop can be used to receive values from a channel until it is closed. Once the channel is closed, the loop automatically exits. This removes the need for this ok value check.
for c := range coins {
fmt.Printf("%s has been teleported!", c)
}
  • Sending to a closed channel will panic. This is to ensure that Goroutines are not blocked waiting to send values to a closed channel, where no more values will ever arrive.

The “Card Shuffle” trick starts a Goroutine which shuffles cards and reveals a card whenever a volunteer instructs it to. 🔀

The channel shuffle reads from a stop channel and reveals a card every time it receives a stop instruction. Once the main Goroutine closes the channel, the ok value received from it will be false and the shuffler will stop reading from the channel. It will then exit, take a bow and close an extra done channel once it is finished.

var tops = []string {"queen of hearts", "four of clubs", "jack of spades"}

func channelShuffle(stop chan bool, done chan bool) {
fmt.Println("The magician starts shuffling the cards.")
time.Sleep(1 * time.Second)

i := 0
for {
_, ok := <- stop
if !ok {
fmt.Println("The magician stops the shuffle and puts the deck down.")
break
}

fmt.Printf("The magician cuts the deck.\n Topcard is %s.\n", tops[i])
time.Sleep(2 * time.Second)
fmt.Println("The magician resumes shuffling the cards.")
i++
}

fmt.Println("The magician takes a bow")
close(done) //close this channel to show we are done and unblock main
}

The shuffler uses the closing of this extra done channel to signal to the main Goroutine that work has successfully completed. Once the channel is closed, the main Goroutine receives the zero value from it, unblocks and shuts down.

The select keyword

The select statement is used to choose from multiple send/receive channel operations.

  • The select statement blocks until one of the send/receive operations on its channels is ready
  • If multiple operations are ready, one of them is chosen at random
  • The default case in a select statement is executed when none of the other cases are ready. This is generally used to prevent the select statement from blocking
select {
case card := <- cards:
fmt.Printf("%s pulled from the deck!\n", card)
case coin:= <- coins:
fmt.Printf("%s pulled from behind your ear!\n", coin)
default:
fmt.Println("No card or coin received!")
}

The “Sawing a Woman in Half” trick starts two Goroutines, which pass a saw between themselves to saw a woman in half until instructed to stop! 😱

(No actual women were harmed during the making of this magic trick 💃 )

The saw performers use the select statement to listen on two channels: one for receiving a saw instruction and one for receiving an instruction to stop the show and take a bow.

func sawPerformer(name string, saw chan bool, done chan bool) {
for {
select {
case <- saw:
fmt.Printf("%s saws the woman.\n", name)
time.Sleep(1 * time.Second)
saw <- true
case <-done:
time.Sleep(1 * time.Second)
fmt.Printf("%s has finished sawing and takes a bow!\n", name)
return
}
}
}

The main Goroutine uses a buffered channel to pass the saw between the saw performers in exactly the same way as the teleporting coin performers. Then, once a predetermined amount of time is spent sawing, it tells the performers to stop.

This trick has a bug — once main closes the done channel, it exits and doesn’t wait for the performers to take their bows and exit gracefully. This can be problematic in production if the Goroutine shutting down is doing important work such as cleaning up resources or locks. Main should wait for both performers to gracefully shut down and take their bows before shutting down.

You can watch a video of the failed trick here.

A possible fix to this problem is to introduce a sync.WaitGroup to block the main Goroutine until the performer Goroutines have finished their bows. I use this fix in the revisited trick. The performer Goroutines are now able to take their bows together and finish the show gracefully!

You wan watch a video of the trick in practice here.

WaitGroups vs Channels

As previously mentioned, channels can be used for synchronisation as well as information passing, so it might be confusing when to use which.

I put together a small list of examples of when to use synchronisation and when to use channels. These are only guidelines, as it is most important to use whichever is simplest and most expressive for your specific problem.

  • Channels are best suited in cases like passing ownership of data, distributing units of work, and communicating async results.
  • Mutexes are best suited for caches, counters and holding state.
  • WaitGroups are best used to ensure that all Goroutines terminate cleanly before cleaning up. Another use for WaitGroups is for cyclic algorithms that involve a set of Goroutines that all work independently for a while, then all wait on a barrier, before proceeding independently again.

Curtain Drop/Conclusions

This brings us to our conclusions and curtain drop. The magic tricks in this session explore the behaviour of channels as well as compare synchronisation mechanisms vs channels.

The full list of magic tricks presented is:

  • Teleporting Coin trick: synchronising Goroutines using channel sends and receives
  • Card Shuffle trick: closing channels to signal work is ended
  • Sawing a Woman in Half trick: using the select to read from multiple channels together with the sync.WaitGroup to wait for Goroutine completion

Talk Materials

My slide deck was designed to be well documented and self standing.

You can find it here. It fully elaborates on the short synopsis I have written in this blogpost.

Stay tuned for more magic tricks in part three, coming soon!

--

--