Channels

Welcome to tutorial no. 22 in Golang tutorial series.

In the previous tutorial, we discussed about how concurrency is achieved in Go using Goroutines. In this tutorial we will discuss about channels and how Goroutines communicate using channels.

What are channels

Channels can be thought of as pipes using which Goroutines communicate. Similar to how water flows from one end to another in a pipe, data can be sent from one end and received from the other end using channels.

Declaring channels

Each channel has a type associated with it. This type is the type of data that the channel is allowed to transport. No other type is allowed to be transported using the channel.

chan T is a channel of type T

The zero value of a channel is nil. nil channels are not of any use and hence the channel has to be defined using make similar to maps and slices.

Let’s write some code that declares a channel.

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	var a chan int
 7	if a == nil {
 8		fmt.Println("channel a is nil, going to define it")
 9		a = make(chan int)
10		fmt.Printf("Type of a is %T", a)
11	}
12}

Run program in playground

The channel a declared in line no. 6 is nil as the zero value of a channel is nil. Hence the statements inside the if condition are executed and the channel is defined. a in the above program is a int channel. This program will output,

channel a is nil, going to define it
Type of a is chan int

As usual, the short hand declaration is also a valid and concise way to define a channel.

1a := make(chan int)

The above line of code also defines an int channel a.

Sending and receiving from a channel

The syntax to send and receive data from a channel is given below,

1data := <- a // read from channel a
2a <- data // write to channel a

The direction of the arrow with respect to the channel specifies whether the data is sent or received.

In the first line, the arrow points outwards from a and hence we are reading from channel a and storing the value to the variable data.

In the second line, the arrow points towards a and hence we are writing to channel a.

Sends and receives are blocking by default

Sends and receives to a channel are blocking by default. What does this mean? When data is sent to a channel, the control is blocked in the send statement until some other Goroutine reads from that channel. Similarly, when data is read from a channel, the read is blocked until some Goroutine writes data to that channel.

This property of channels is what helps Goroutines communicate effectively without the use of explicit locks or conditional variables that are quite common in other programming languages.

It’s ok if this doesn’t make sense now. The upcoming sections will add more clarity on how channels are blocking by default.

Channel example program

Enough of theory :). Let’s write a program to understand how Goroutines communicate using channels.

We will actually rewrite the program we wrote when learning about Goroutines using channels here.

Let me quote the program here from the last tutorial.

 1package main
 2
 3import (  
 4    "fmt"
 5    "time"
 6)
 7
 8func hello() {  
 9    fmt.Println("Hello world goroutine")
10}
11func main() {  
12    go hello()
13    time.Sleep(1 * time.Second)
14    fmt.Println("main function")
15}

Run program in playground

This was the program from the last tutorial. We use a sleep here to make the main Goroutine wait for the hello Goroutine to finish. If this doesn’t make sense to you, I recommend reading the tutorial on Goroutines

We will rewrite the above program using channels.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func hello(done chan bool) {
 8	fmt.Println("Hello world goroutine")
 9	done <- true
10}
11func main() {
12	done := make(chan bool)
13	go hello(done)
14	<-done
15	fmt.Println("main function")
16}

Run program in playground

In the above program, we create a done bool channel in line no. 12 and pass it as a parameter to the hello Goroutine. In line no. 14 we are receiving data from the done channel. This line of code is blocking which means that until some Goroutine writes data to the done channel, the control will not move to the next line of code. Hence this eliminates the need for the time.Sleep which was present in the original program to prevent the main Goroutine from exiting.

The line of code <-done receives data from the done channel but does not use or store that data in any variable. This is perfectly legal.

Now we have our main Goroutine blocked waiting for data on the done channel. The hello Goroutine receives this channel as a parameter, prints Hello world goroutine and then writes to the done channel. When this write is complete, the main Goroutine receives the data from the done channel, it is unblocked and then the text main function is printed.

This program outputs

Hello world goroutine
main function

Let’s modify this program by introducing a sleep in the hello Goroutine to better understanding this blocking concept.

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func hello(done chan bool) {
 9	fmt.Println("hello go routine is going to sleep")
10	time.Sleep(4 * time.Second)
11	fmt.Println("hello go routine awake and going to write to done")
12	done <- true
13}
14func main() {
15	done := make(chan bool)
16	fmt.Println("Main going to call hello go goroutine")
17	go hello(done)
18	<-done
19	fmt.Println("Main received data")
20}

Run in playground

In the above program, we have introduced a sleep of 4 seconds to the hello function in line no. 10.

This program will first print Main going to call hello go goroutine. Then the hello Goroutine will be started and it will print hello go routine is going to sleep. After this is printed, the hello Goroutine will sleep for 4 seconds and during this time main Goroutine will be blocked since it is waiting for data from the done channel in line no. 18 <-done. After 4 seconds hello go routine awake and going to write to done will be printed followed by Main received data.

Another example for channels

Let’s write one more program to understand channels better. This program will print the sum of the squares and cubes of the individual digits of a number.

For example, if 123 is the input, then this program will calculate the output as

squares = (1 * 1) + (2 * 2) + (3 * 3) cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) output = squares + cubes = 50

We will structure the program such that the squares are calculated in a separate Goroutine, cubes in another Goroutine and the final summation happens in the main Goroutine.

 1package main
 2
 3import (  
 4    "fmt"
 5)
 6
 7func calcSquares(number int, squareop chan int) {  
 8    sum := 0
 9    for number != 0 {
10        digit := number % 10
11        sum += digit * digit
12        number /= 10
13    }
14    squareop <- sum
15}
16
17func calcCubes(number int, cubeop chan int) {  
18    sum := 0 
19    for number != 0 {
20        digit := number % 10
21        sum += digit * digit * digit
22        number /= 10
23    }
24    cubeop <- sum
25} 
26
27func main() {  
28    number := 589
29    sqrch := make(chan int)
30    cubech := make(chan int)
31    go calcSquares(number, sqrch)
32    go calcCubes(number, cubech)
33    squares, cubes := <-sqrch, <-cubech
34    fmt.Println("Final output", squares + cubes)
35}

Run program in playground

The calcSquares function in line no. 7 calculates the sum of the squares of the individual digits of the number and sends it to the squareop channel. Similarly the calcCubes function in line no. 17 calculates the sum of cubes of the individual digits of the number and sends it to the cubeop channel.

These two functions are run as separate Goroutines in line no. 31 and 32 and each is passed a channel to write to as the parameter. The main Goroutine waits for data from both these channels in line no. 33. Once the data is received from both the channels, they are stored in squares and cubes variables and the final output is computed and printed. This program will print

Final output 1536

Deadlock

One important factor to consider while using channels is deadlock. If a Goroutine is sending data on a channel, then it is expected that some other Goroutine should be receiving the data. If this does not happen, then the program will panic at runtime with Deadlock.

Similarly, if a Goroutine is waiting to receive data from a channel, then some other Goroutine is expected to write data on that channel, else the program will panic.

1package main
2
3
4func main() {
5	ch := make(chan int)
6	ch <- 5
7}

Run program in playground

In the program above, a channel ch is created and we send 5 to the channel in line no. 6 ch <- 5. In this program no other Goroutine is receiving data from the channel ch. Hence this program will panic with the following runtime error.

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/sandbox046150166/prog.go:6 +0x50

Unidirectional channels

All the channels we discussed so far are bidirectional channels, that is data can be both sent and received on them. It is also possible to create unidirectional channels, that is channels that only send or receive data.

 1package main
 2
 3import "fmt"
 4
 5func sendData(sendch chan<- int) {
 6	sendch <- 10
 7}
 8
 9func main() {
10	sendch := make(chan<- int)
11	go sendData(sendch)
12	fmt.Println(<-sendch)
13}

Run program in playground

In the above program, we create send only channel sendch in line no. 10. chan<- int denotes a send only channel as the arrow is pointing to chan. We try to receive data from a send only channel in line no. 12. This is not allowed and when the program is run, the compiler will complain stating,

./prog.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)

All is well but what is the point of writing to a send only channel if it cannot be read from!

This is where channel conversion comes into use. It is possible to convert a bidirectional channel to a send only or receive only channel but not the vice versa.

 1package main
 2
 3import "fmt"
 4
 5func sendData(sendch chan<- int) {
 6	sendch <- 10
 7}
 8
 9func main() {
10	chnl := make(chan int)
11	go sendData(chnl)
12	fmt.Println(<-chnl)
13}

Run program in playground

In line no. 10 of the program above, a bidirectional channel chnl is created. It is passed as a parameter to the sendData Goroutine in line no. 11. The sendData function converts this channel to a send only channel in line no. 5 in the parameter sendch chan<- int. So now the channel is send only inside the sendData Goroutine but it’s bidirectional in the main Goroutine. This program will print 10 as the output.

Closing channels and for range loops on channels

Senders have the ability to close the channel to notify receivers that no more data will be sent on the channel.

Receivers can use an additional variable while receiving data from the channel to check whether the channel has been closed.

v, ok := <- ch

In the above statement ok is true if the value was received by a successful send operation to a channel. If ok is false it means that we are reading from a closed channel. The value read from a closed channel will be the zero value of the channel’s type. For example, if the channel is an int channel, then the value received from a closed channel will be 0.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func producer(chnl chan int) {
 8	for i := 0; i < 10; i++ {
 9		chnl <- i
10	}
11	close(chnl)
12}
13func main() {
14	ch := make(chan int)
15	go producer(ch)
16	for {
17		v, ok := <-ch
18		if ok == false {
19			break
20		}
21		fmt.Println("Received ", v, ok)
22	}
23}

Run program in playground

In the program above, the producer Goroutine writes 0 to 9 to the chnl channel and then closes the channel. The main function has an infinite for loop in line no.16 which checks whether the channel is closed using the variable ok in line no. 18. If ok is false it means that the channel is closed and hence the loop is broken. Else the received value and the value of ok is printed. This program prints,

Received  0 true
Received  1 true
Received  2 true
Received  3 true
Received  4 true
Received  5 true
Received  6 true
Received  7 true
Received  8 true
Received  9 true

The for range form of the for loop can be used to receive values from a channel until it is closed.

Let’s rewrite the program above using a for range loop.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func producer(chnl chan int) {
 8	for i := 0; i < 10; i++ {
 9		chnl <- i
10	}
11	close(chnl)
12}
13func main() {
14	ch := make(chan int)
15	go producer(ch)
16	for v := range ch {
17		fmt.Println("Received ",v)
18	}
19}

Run program in playground

The for range loop in line no. 16 receives data from the ch channel until it is closed. Once ch is closed, the loop automatically exits. This program outputs,

Received  0
Received  1
Received  2
Received  3
Received  4
Received  5
Received  6
Received  7
Received  8
Received  9

The program from Another example for channels section can be rewritten with more code reusability using for range loop.

If you take a closer look at the program you can notice that the code which finds the individual digits of a number is repeated in both calcSquares function and calcCubes function. We will move that code to its own function and call it concurrently.

 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func digits(number int, dchnl chan int) {
 8	for number != 0 {
 9		digit := number % 10
10		dchnl <- digit
11		number /= 10
12	}
13	close(dchnl)
14}
15func calcSquares(number int, squareop chan int) {
16	sum := 0
17	dch := make(chan int)
18	go digits(number, dch)
19	for digit := range dch {
20		sum += digit * digit
21	}
22	squareop <- sum
23}
24
25func calcCubes(number int, cubeop chan int) {
26	sum := 0
27	dch := make(chan int)
28	go digits(number, dch)
29	for digit := range dch {
30		sum += digit * digit * digit
31	}
32	cubeop <- sum
33}
34
35func main() {
36	number := 589
37	sqrch := make(chan int)
38	cubech := make(chan int)
39	go calcSquares(number, sqrch)
40	go calcCubes(number, cubech)
41	squares, cubes := <-sqrch, <-cubech
42	fmt.Println("Final output", squares+cubes)
43}

Run program in playground

The digits function in the program above now contains the logic for getting the individual digits from a number and it is called by both calcSquares and calcCubes functions concurrently. Once there are no more digits in the number, the channel is closed in line no. 13. The calcSquares and calcCubes Goroutines listen on their respective channels using a for range loop until it is closed. The rest of the program is the same. This program will also print

Final output 1536

This brings us to the end of this tutorial. There are few more concepts in channels such as buffered channels, worker pools and select. We will discuss them in separate tutorials of their own. Thanks for reading.

I hope you liked this tutorial. Please leave your feedback and comments. Please consider sharing this tutorial on twitter and LinkedIn. Have a good day.

Next tutorial - Buffered Channels and Worker Pools