Anatomy of Channels in Go - Concurrency in Go

Go provides chan keyword to create a channel. A channel can transport data of only one data type

A channel is a communication object using which goroutines can communicate with each other. Technically, a channel is a data transfer pipe where data can be passed into or read from. Hence one goroutine can send data into a channel, while other goroutines can read that data from the same channel.

Channels allow go routines to communicate with each other. You can think of a channel as a pipe, from which go routines can send and receive information from other go routines.
myFirstChannel := make(chan string)

Go routines can send and receive on a channel. This is done through using an arrow (<-) that points in the direction that the data is going.
myFirstChannel <- "hello" // Send myVariable := <- myFirstChannel // Receive

Now by using a channel, we can have our ore finding gopher send what they discover to our ore breaking gopher right away, without waiting to discover everything.
I’ve updated the example so the finder code and miner functions are set up as unnamed functions. If you’ve never seen lambda functions don’t focus too much on that part of the program, just know that each of the functions are being called with the go keyword so they’re being ran on their own go routine.

What’s important is to notice how the go routines are passing data between each other using the channel, oreChan. Don’t worry, I’ll explain unnamed functions at the end.
func main() { theMine := [5]string{“ore1”, “ore2”, “ore3”} oreChan := make(chan string) // Finder
go func(mine [5]string) { for _, item := range mine { oreChan <- item //send } }(theMine) // Ore Breaker
go func() { for i := 0; i < 3; i++ { foundOre := <-oreChan //receive fmt.Println(“Miner: Received “ + foundOre + “ from finder”) } }()
<-time.After(time.Second * 5) // Again, ignore this for now }

In the output below, you can see that our Miner receives the pieces of “ore” one at a time from reading off the ore channel three times.
Miner: Received ore1 from finder
Miner: Received ore2 from finder
Miner: Received ore3 from finder

Great, now we can send data between different go routines (gophers) in our program. Before we start writing complex programs with channels, lets first cover some crucial to understand channel properties.

Channel Blocking

Channels block go routines in various situations. This allows our go routines to sync up with each other for a moment, before going on their independently merry way.

Blocking on a Send


Once a go routine (gopher) sends on a channel, the sending go routine blocks until another go routine receives what was sent on the channel.

Blocking on a Receive

Similar to blocking after sending on a channel, a go routine can block waiting to get a value from a channel, with nothing sent to it yet.

Blocking can be a bit confusing at first, but you can think of it like a transaction between two go routines (gophers). Whether a gopher is waiting for money or sending money, it will wait until the other partner in the transaction shows up.

Now that we have an idea on the different ways a go routine can block while communicating through a channel, lets discuss the two different types of channels: unbuffered, and buffered. Choosing what type of channel you use can change how your program behaves.

Unbuffered Channels

We’ve been using unbuffered channels in all previous examples. What makes them unique is that only one piece of data fits through the channel at a time.

Buffered Channels

In concurrent programs, timing isn’t always perfect. In our mining example, we could run into a situation where our finding gopher can find 3 pieces of ore in the time it takes the breaking gopher to process one piece of ore.

In order to not let the surveying gopher spend most of its time waiting to send the breaking gopher some ore until it finishes, we can use a buffered channel. Lets start by making a buffered channel with a capacity of 3.
bufferedChan := make(chan string, 3)

Buffered channels work similar to unbuffered channels, but with one catch — we can send multiple pieces of data to the channel before needing another go routine to read from it.
bufferedChan := make(chan string, 3)go func() { bufferedChan <- "first" fmt.Println("Sent 1st") bufferedChan <- "second" fmt.Println("Sent 2nd") bufferedChan <- "third" fmt.Println("Sent 3rd") }()

<-time.After(time.Second * 1)go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }()

The order of printing between our two go routines would be:
Sent 1st Sent 2nd Sent 3rd Receiving.. first second third

To keep things simple, we won’t be using buffered channels in our final program, but it’s important to know what types of channels are available in your concurrency tool belt.
Note: Using buffered channels doesn’t prevent blocking from happening. For example, if the finding gopher is 10 times faster than the breaker, and they communicate through a buffered channel of size 2, the finding gopher will still block multiple times in the program.

Putting it all Together

Now with the power of go routines and channels, we can write a program that takes full advantage of multiple threads using Go’s concurrency primitives.
theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string)// Finder go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item //send item on oreChannel } } }(theMine)// Ore Breaker go func() { for i := 0; i < 3; i++ { foundOre := <-oreChannel //read from oreChannel fmt.Println("From Finder: ", foundOre) minedOreChan <- "minedOre" //send to minedOreChan } }()// Smelter go func() { for i := 0; i < 3; i++ { minedOre := <-minedOreChan //read from minedOreChan fmt.Println("From Miner: ", minedOre) fmt.Println("From Smelter: Ore is smelted") } }()<-time.After(time.Second * 5) // Again, you can ignore this

The output of this program is the following:
From Finder: ore
From Finder: ore
From Miner: mined Ore
From Smelter: Ore is smelted
From Miner: minedOre
From Smelter: Ore is smelted
From Finder: ore
From Miner: minedOre
From Smelter: Ore is smelted

This has been a great improvement from our original example! Now each of our functions are running independently on their own go routines. Also, every time there’s a piece of ore processed, it gets carried on to the next stage of our mining line.

For the sake of keeping the focus on understanding the basics of channels and go routines, there was some important information I didn’t mention above- which, if you don’t know, could cause some trouble when you start programming. Now that you have an understanding of how go routines and channels work, let’s go over some information you should know before you start coding with go routines and channels.

Never miss a post from Snehal Kumar, when you sign up for Ednsquare.