Intro
Channels are a core component of Go’s concurrency model. It allows goroutines to communicate safely and synchronize their execution. Channel is super useful in any of your programs thanks to its superpower.
You may imagine a channel in Go as a canal, where cargo ships travel from one side of the canal to the other, delivering goods in a orderly manner. Bu for a Go channel, it is a stream of information that flows between goroutines. We send data along the channel like loading cargo onto ships, and read the data downstream like unloading cargo at the destination.
Get started
Channel creation
Creating Go Channels is easy. Here would create a channel dataStream
which allows any data (interface{}
means any
in Golang) to be read or written. Any we also create a channel intStream
which allows only int
data to be read or written.
1dataStream := make(chan interface{})2intStream := make(chan int)
Channel read & write
read:
syntax: val, ok := <- ch
val
: value read from the channel.ok
: a booleanok
istrue
when:- The channel is open and a value is successfully received from the channel.
- The channel is closed but there are buffered values in the channel
ok
isfalse
when:- The channel is closed and there are no more values left to receive.
write:
syntax: ch <- val
example:
1func chanReadWriteExample() {2 intCh := make(chan int)3 go func() {4 intCh <- 1 // write to intCh5 close(intCh)6 }()7 for _ = range 2 {8 val, ok := <-intCh // read from intCh9 fmt.Printf("(%v): %d\n", ok, val)10 }11}12
13// output:14// (true): 115// (false): 0
Chanel closing
Closing a channel is also one of the ways you can signal multiple goroutines simultaneously.
A closed channel can still be received from, but it will always return zero values after the last value has been received.
-
Exmple:
1func closeChannelAsSignal() {2startRace := make(chan interface{})3wg := sync.WaitGroup{}45for i := 0; i < 5; i++ {6wg.Add(1)7go func(id int) {8defer wg.Done()9<-startRace // Wait for start signal10fmt.Printf("Runner %d started (%s)\n", id, time.Now().Format("15:04:05.000"))11}(i)12}1314fmt.Printf("Preparing the race... (%s)\n", time.Now().Format("15:04:05.000"))15time.Sleep(2 * time.Second)5 collapsed lines16fmt.Printf("...GO! (%s)\n", time.Now().Format("15:04:05.000"))1718close(startRace) // Signal all runners to start19wg.Wait() // Wait for all runners to finish20}Terminal window 1# outputs:2Preparing the race... (16:47:49.317)3...GO! (16:47:51.317)4Runner 3 started (16:47:51.318)5Runner 4 started (16:47:51.318)6Runner 2 started (16:47:51.318)7Runner 1 started (16:47:51.318)8Runner 0 started (16:47:51.318)
Types of channels
1. Bidirection & unidirection
1) bidirectional channels
The channels we defined above are all bidirectional channels where we can read and write to the channel.
Bidirectional channel:
1bidirectionalCh := make(chan interface{})
2) unidirectional channels
The unidirectional channel means you can either write to or read from the channel.
-
Write-only channel:
1writeOnlyCh := make(chan<- interface{}) -
Read-only channel:
1readOnlyCh := make(<-chan interface{})
Initializing unidirectional channels is relatively uncommon in real-world scenarios. They are more often used as function parameters or return types. You may ask but how? Go will implicitly convert bidirectional channels to unidirectional channels.
For example, in the Consumer-Producer pattern, consumers typically operate on a read-only channel, while producers use a write-only channel. We would explore that a bit later.
2. Unbuffered & buffered
1) unbuffered channels
-
Define: a channel created with a capacity of 0 to store data.
-
Create:
1// The two channels have equivalent functionalities2a := make(chan int)3b := make(chan int, 0) -
Analog: there is a narrow canal without docking area. Each ship (data) arriving at the canal must be immediately unloaded by a waiting docker (goroutine). If no docker is ready, the ship must wait at the entrance. Similarly, sending to an unbuffered channel blocks until another goroutine is ready to receive.
-
How it works:
- Sender Attempts to Send: when the sending goroutine sends a value into an unbuffered channel, it waits until another goroutine is ready to receive that value.
- Receiver Attempts to Receive: when the receing goroutine tries to receive from an unbuffered channel, it waits until another goroutine sends a value.
-
Case study: deadlock
The following code leads to the deadlock. Since it is an unbuffered channel, when executing
intCh <- 1
, the main goroutine would be blocked until another goroutine is ready to receive the data. However, the receiver of the channel is the main goroutine again. The main goroutine would be blocked forever atintCh <- 1
, causing the deadlock.1func main() {2intCh := make(chan int)3defer close(intCh)45intCh <- 167val, ok := <-intCh8fmt.Printf("(%v): %d\n", ok, val)9}
2) buffered channels
-
Define: channels that are given a capacity to store data **when they’re instantiated.
-
Analog: there is a canal with a docking bay that can hold several ships. Ships can arrive and wait in the docks (buffer) even if no docke is immediately available. Ships are blocked at the entrance only when the docks are all occupied. Similarly, sending to a buffered channel only blocks when the buffer is full, and receiving blocks only when the buffer is empty.
-
How it works: sending to a buffered channel blocks only if the buffer is full, and receiving blocks only if the buffer is empty.
-
Create:
1a := make(chan int, 5) -
Example: Producer-Consumer
In this example, the for loop over the channel breaks when:
- The channel is closed, and
- All values from the channel have been received.
1func bufferedChannelExample() {2// Create a buffer to store output for synchronized printing3var stdoutBuff bytes.Buffer4defer stdoutBuff.WriteTo(os.Stdout)56// Create buffered channel with capacity 47intStream := make(chan int, 4)89// Producer goroutine10go func() {11defer close(intStream)12defer fmt.Fprintln(&stdoutBuff, "Producer Done.")13for i := 0; i < 6; i++ {14fmt.Fprintf(&stdoutBuff, "+ Sending: %d (%s)\n", i, time.Now().Format("15:04:05.000"))15intStream <- i // Will block when buffer is full (after 4 items)10 collapsed lines16}17}()1819time.Sleep(100 * time.Millisecond)2021// Consumer: read until channel is closed and all buffered values are received22for integer := range intStream {23fmt.Fprintf(&stdoutBuff, "- Received %v (%s).\n", integer, time.Now().Format("15:04:05.000"))24}25}Terminal window 1# outputs:2+ Sending: 0 (16:32:32.793)3+ Sending: 1 (16:32:32.793)4+ Sending: 2 (16:32:32.793)5+ Sending: 3 (16:32:32.793)6+ Sending: 4 (16:32:32.793)7- Received 0 (16:32:32.894).8- Received 1 (16:32:32.894).9- Received 2 (16:32:32.894).10- Received 3 (16:32:32.894).11- Received 4 (16:32:32.894).12+ Sending: 5 (16:32:32.894)13Producer Done.14- Received 5 (16:32:32.894).
Results of channel op
At first glance of this table, it looks as channes could be dangerous to use. However, after examining the motivation of these results and framing the use of channels, it becomes less scary and makes a lot of sense. Here we would introduce how to organize different types of channels.
The first thing we should do is to assgin channel ownership. The ownership is defined as being a goroutine that instantiates, writes and closes a channel. Then, channel owners have a write-access view into the channel (chan
or chan<-
), while the channel utilizers only have a read-only view (<-chan
).
The second thing is that as a consumer, you should handle the fact that reads can and will block.
To see the full example of code, you can visit: goChannel.
References
- Concurrency in Go: Tools and Techniques for Developers 1st Edition ****by Katherine Cox-Buday
- https://medium.com/goturkiye/concurrency-in-go-channels-and-waitgroups-25dd43064d1