This is the second part in the series of articles about Golang Concurrency, and this will be focussed on Channels.
We discussed about Goroutines in depth in the previous article, diving into its differences with OS Threads, some common pitfalls and race conditions. Now it is the time to dive into Channels, to see how goroutines interact with each other.
All Articles of the Series :
1. Golang Concurrency : Goroutines
2. Golang Concurrency : Channels
3. Golang Concurrency: Select and For Range Channel
Channels
Imagine you are in a manufacturing plant. There are certain sections/units, which are each responsible for one particular task (these are like goroutines
). Now instead of shouting across the plant, these sections or units are connected by conveyor belts to transfer messages or items (data) among each other. In essence these conveyor belts are the channels passing messages from one unit (goroutine) to other.
Channels are a safe way for goroutines to communicate and keep each other in sync.
Normal OS Threads communicate by sharing memory. Threads are unaware of each other they just know memory, and they all communicate by sharing memory. It has it’s own repercussions, primarily deadlocks arising due to the usage of locks to control access to the shared memory or data.
Golang is free from this problem. It has channels, where in a goroutine can pass data to a particular channel, and another goroutine can read data from it.
“Don’t communicate by sharing memory, share memory by communicating”, Read this somewhere and it particularly fits in the horizon of Golang, Goroutines and channels.
How to create Channel?
chan1 := make(chan int)
This code snippet creates a new channel. Please note the type int
, it tells that the channel is going to communicate data of type int.
Basic Operations
There are 2 basic operations that can be performed on a channel:
1. Sending data to channel
2. Receiving data from channel
chan1 <- 1 //this sends data (1) to chan1
dataReceived := <-chan1 // this received data(1) from chan1
There is one very important thing to note here. When we send data to a channel from a goroutine, that goroutine pauses, till there is some other goroutine that is ready to receive data. And similar thing happens when a gouroutine is receiving a data, it pauses till there is another goroutine that is sending data to that channel. This very property of channels make them so useful in synchronising goroutines.
Let us bind everything in an example to visualize it better.
package main
import (
"fmt"
"time"
)
func listenChan1(ch chan int) {
fmt.Println("Sending data to channel, data is 1")
time.Sleep(1 * time.Second) //to show the blocking nature of channel
ch <- 1
fmt.Println("1 is sent")
}
func main() {
chan1 := make(chan int)
go listenChan1(chan1)
fmt.Println("Waiting for data from channel")
dataRcvd := <-chan1
fmt.Println("Received data from chan1, data is", dataRcvd)
}
// Output
Waiting for data from channel
Sending data to channel, data is 1
/* 1 second pause */
1 is sent
Received data from chan1, data is 1
Let us go through the code in more detail:
- The main function (main goroutine), sets up an unbuffered channel (more on this later), and then schedules a goroutine to send a value to this channel.
- When the main goroutine hits the line
dataRcvd := <-chan1
, it waits until a value is sent to that channel. - The goroutine that the main has fired in step 1, is asked to wait for 1 second, and then send
1
to that channel. - The main goroutine is hence blocked or paused for 1 second, till the goroutine (fired in step 1) sends data to the channel the main is listening to, and post that, it receives the data and prints it.
Closing channels
Closing a channel is as crucial as opening it when it comes to Goroutines. It tells the listener that the job that this channel (and hence those goroutines) were supposed to do is now finished, and no more data will be sent to this channel. If some goroutine tries to send data to this channel, the whole program will panic.
However, listening from a closed channel wont trigger a panic. It will actually return the zero value
or default value
of the channel’s datatype. So if the goroutine is listening to channel of type int
, and that channel is closed, the receiver will receive 0
.
Please make a note that closing a channel is a very definitive task, you cannot reopen a closed channel.
Let us now see how to check if a channel is closed.
package main
import (
"fmt"
"time"
)
func listenChan1(ch chan int) {
fmt.Println("Sending data to channel, data is 1")
time.Sleep(1 * time.Second)
close(ch)
}
func main() {
chan1 := make(chan int)
go listenChan1(chan1)
fmt.Println("Waiting for data from channel")
dataRcvd, ok := <-chan1 // verbose way of listening from a channel
if !ok {
fmt.Println("The channel has been closed")
} else {
fmt.Println("Received data from chan1, data is", dataRcvd)
}
}
// Output
Waiting for data from channel
Sending data to channel, data is 1
/* 1 second pause */
The channel has been closed
Unfortunately, there is no direct way of getting the state of a channel in golang, so we will employ a more verbose way of receiving data from a channel. The syntax is dataRcvd, ok := <-chan1
. The extra variable ok
is of type bool, and it tells if the channel is active or closed. If the value of ok
is false
it means that the channel has been closed, and the value returned is just the default value for the underlying datatype and hence can be ignored.
Types of Channels
Two major type of channels in Golang are : Unbuffered
and Buffered
.
Channels also have a categorisation based on the direction of data called as directional channel, but they are just added syntactical variations over the core channel data type provided by Go Compiler.
Let us understand the different type of channels by their behaviour under various circumstances :
Unbuffered Channels
These are the type of channels that we have used thus far for all the examples. It is a channel that we get when we don’t specify the size of the channel, that means the size of unbuffered channel is 0.
uchan1 := make(chan int)
uchane1 := make(chan int, 0)
Immediate Blocking
They can’t hold (store) any data, as soon as data is provided to them, they just want some other goroutine to receive it and clear the channel, and till the time it does not happen, they are blocked.
Synchronisation
These channels help a lot in synchronisation, as soon as some value is sent to a channel, there must be another goroutine waiting to listen from it.
Deadlock
When the above condition is not met, that is, there is no goroutine to receive data from the channel, the program enters deadlock, it does not panics, but enters in the deadlock state
Buffered Channels
Buffered Channels are just Unbuffered Channels, but with some storage or size.
chan1 := make(chan int, 3)
This means that this channel won’t be blocking as soon as some data is sent to it, it has internal space to accommodate 3 values before it turns to a blocking channel. So if a goroutine is sending data to this channel, it can send upto 3 values without worrying if there is another goroutine listening to it. However, once the limit is reached (3 values have been sent to this channel), there must be a receiver listening to it and clearing data from it or else it will turn blocking.
Let us see this in code:
package main
import (
"fmt"
)
func listenChan1(ch chan int) {
fmt.Println("Gorutine: Waiting for data from channel")
value := <-ch
fmt.Println("Gorutine: got data from channel, data is ", value)
}
func main() {
chan1 := make(chan int, 3)
go listenChan1(chan1)
chan1 <- 1
chan1 <- 2
chan1 <- 3
fmt.Println("channel is full now")
chan1 <- 4
fmt.Println("last data has gone through")
}
//Output
channel is full now
Gorutine: Waiting for data from channel
Gorutine: got data from channel, data is 1
last data has gone through
- The code creates a channel of length 3.
- Fires a gorutine
- Main quickly pushes 3 values to that channel. And then prints that the channel is full.
- Now if you observe closed, the next lines (inserting value 4 to the channel, and printing the last data line) do not execute, before the goroutine (fired in line 2) received a value from the channel and prints
Gorutine: got data from channel, data is 1
. When this happens, one data item is removed from the channel, and it is now ready to accommodate another value. - Only then
chan1 <- 4
gets executed.
Capacity and Length
Any unbuffered channel has to important attributes to it.
- Capacity: The number of data items that the channel can store.
- Length: The number of data items currently in the channel.
Length will always be less than or equal to the capacity.
So now using these terms, an unbuffered channel is non blocking, till it’s length is less than it’s capacity. Once the length is equal to its capacity, the channel becomes blocking, and it need some goroutine to read data from it, so that it can flust a data item, and length decreases to value less than it’s capacity.
Directional Channels
Directional channels are again just syntactical attributes. They represent if the channel is to be used for sending data or receiving data. By default, the channels are bidirectional, but Go language provides a way of restricting them, enforcing perfect data flows.
As stated earlier, they are not different types of channels, they are just compile-time constraints over normal channels.
package main
import (
"fmt"
"time"
)
func listenToChan2(ch chan int) {
fmt.Println("Waiting for data from channel 2")
data := <-ch
fmt.Println("Got the data from channel 2", data)
}
func sendToChan1(ch chan<- int) {
fmt.Println("Gorutine: Sending data to channel")
ch <- 1
}
func listenChan1(ch <-chan int) {
fmt.Println("Listener: Listeneing from chan1")
dataRcvd, ok := <-ch
if !ok {
fmt.Println("The channel has been closed")
} else {
fmt.Println("Listener: Received data from chan1, data is", dataRcvd)
}
}
func main() {
chan1 := make(chan int)
go sendToChan1(chan1)
go listenChan1(chan1)
time.Sleep(1 * time.Second) //pause till goroutines execute
fmt.Println("all done")
}
//Output
Gorutine: Sending data to channel
Listener: Listeneing from chan1
Listener: Received data from chan1, data is 1
all done
As you can see, we are not creating different channels, we have just created one channel chan1
and passing it to both sendToChan1
and listenChan1
.
The change is in the signature of the goroutine functions. While the sendToChan1
` is expecting a chan like ch chan<- int
, the listenChan1 is expecting ch <-chan int
.chan<- int
means that in this goroutine the channel is send only (this goroutine can only send data to this channel).<-chan in
means that in this goroutine the channel is read only(this goroutine can only read data from this channel)
This helps us in writing better and cleaner code, and adds a layer of transparency as to the role of a channel in a goroutine.
And this is where we rest our case with channels. There are far more topics to cover as to how read data from channels iteratively and some more techniques, but this article has already grown huge, we will cover these aspects in the next part of the series.
I consistently write about Golang and System Design, follow me on twitter and linkedIn to stay connected.
0 Comments