Learn Go: Introduction to Channels in Golang #29

In this tutorial, we are going to be looking at how you can use channels within your Go applications.

Gopher

Lets go!!

Published · Feb 9

Previous Tutorial: Concurrency With Golang Goroutines #28
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.

Channels are these pipes via which different routines can send and receive values of a certain type between different goroutines.
. . .

What are channels


Channels can be thought 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 another 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.

A channel can be made with the statement:
myFirstChannel := make(chan string, size) // size is optional

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.


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.
package main

import (
"fmt"
"time"
)

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. Channels are blocking by default.

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.

We will be looking at the buffered channels in next tutorial

. . .

Channel 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.
package main func main() { ch := make(chan int) ch <- 5 }


In the program above, a channel ch is created and we send 5 to the channel in line 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/sandbox249677995/main.go:6 +0x80
. . .

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.
package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { sendch := make(chan<- int) go sendData(sendch) fmt.Println(<-sendch) }


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
main.go:11: 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.

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

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

channels can be close by calling close on the channel as follows
close(ch)

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.
package main

import (
"fmt"
)

func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for {
v, ok := <-ch
if !ok {
fmt.Println("channel closed")
break

}
fmt.Println("Received ", v, ok)
}
}


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.
. . .

For loop over Channels

The for range form of the for loop can be used to receive values from a channel until it is closed.
package main import ( "fmt" ) func producer(chnl chan int) { for i := 0; i < 10; i++ { chnl <- i } close(chnl) } func main() { ch := make(chan int) go producer(ch) for { v, ok := <-ch if ok == false { break } fmt.Println("Received ", v, ok) } }


Lets rewrite the program above using a for range loop.
package main

import (
"fmt"
)

func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ", v)
}
}


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.
. . .

Select case over Channels

A select statement is like a switch statement for channels. The things to note about the select statement is:
  • It runs the first non-blocking case
  • If more than one case is non-blocking, it runs one of it randomly
  • If all cases are blocking, the whole block becomes blocking until one of the cases unblocks. Unless a default case is given (More on this)

Looking at the code:
package main

import (
"fmt"
"time"
)

func main() {
pipe := make(chan string, 2)
second := make(chan string, 2)
go goroutines(second, time.Second*3, "First goroutine")
go goroutines(pipe, time.Second*2, "Second goroutine")

select {
case res := <-pipe:
fmt.Println(res)
case res := <-second:
fmt.Println(res)
}
}

func goroutines(out chan<- string, wait time.Duration, name string) {
time.Sleep(wait)
out <- name
}


A breakdown of what happens:


Default case
If we were to update the code above with something like this
package main

import (
"fmt"
"time"
)

func main() {
pipe := make(chan string, 2)
second := make(chan string, 2)
go goroutines(second, time.Second*3, "First goroutine")
go goroutines(pipe, time.Second*2, "Second goroutine")

select {
case res := <-pipe:
fmt.Println(res)
case res := <-second:
fmt.Println(res)
default:
fmt.Println("None are ready")
}
}

func goroutines(out chan<- string, wait time.Duration, name string) {
time.Sleep(wait)
out <- name
}
The select statement blocks (if all cases are blocking), if there is no available default case. If there is a default case, it runs that immediately.
. . .

Gotchas

  • A send to a nil channel blocks forever
var c chan string c <- "Hello, World!"
// fatal error: all goroutines are asleep - deadlock!
  • A receive from a nil channel blocks forever
var c chan string fmt.Println(<-c)
// fatal error: all goroutines are asleep - deadlock!
  • A send to a closed channel panics
var c = make(chan string, 1)
c <- "Hello, World!"
close(c)
c <- "Hello, Panic!" // panic: send on closed channel
  • A receive from a closed channel returns the zero value immediately
var c = make(chan int, 2)
c <- 1
c <- 2
close(c)
for i := 0; i < 3; i++ {
fmt.Printf("%d ", <-c)
} // 1 2 0
. . .

Conclusion

Hopefully, this article helped get a good grasp of goroutines, goroutine communication and how they are scheduled. I will be writing a follow-up article on the inner workings of channels to better understand how to use them to co-ordinate your goroutines better.

Till then, try and play around with goroutines and explore different results, or you could even try and explore parallelism by setting the GOMAXPROCS to the number of cores available in your CPU (Caution advised).
. . .
Next Tutorial: Buffered Channels in Golang #30

. . .

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