Channels are a way to send and receive data between Go routines. It acts as pipes that connects different goroutines with each other for data exchange.

What are Go channels?

Channels are a way through which different go-routines communicate. It’s like a data exchange mechanism in Go which allows us to exchange data between different parts of go-routine. Channels make concurrent programming easier. Traditional threading models were communicating by sharing memory, however, channels try to avoid that because that can result in nasty bugs. In traditional concurrent programming, these shared memory is locked by threads. There is possiblity of deadlock and thread contention over that data.

Go routines and channels allow access to data using channels. Channels share memory by communicating which ensures that at a given time, only one routine has access to the memory. The communication in the channels is bi-directional, that is you can send and receive values using the same channel. When you send data to a channel, the program execution control is blocked until another goroutine reads from this channel. This way go-routines synchronize without the use of locks or conditional variables. The same thing happens when reading from a channel. The reading is blocked until another goroutine writes to the channel.

Each channels holds data of particular data type. Syntax for this looks like below. We use chan keyword to declare channel of specific type. For example, below is example of string data type channel.

1var c chan string
2c := make(chan string)

Let’s look at some common operations with channels.

  1. You can send a value to a channel using <- operator. For this to work, the data type of input must match the channel type.
  2. You can receive a value from a channel using <-ch
  3. Close a channel using close(ch) function.
 1package main
 2
 3import (
 4	"fmt"
 5)
 6
 7func main() {
 8	var ch chan string
 9	ch = make(chan string)
10	go hello(ch)
11
12	// receive message from the channel
13	msg := <-ch
14	fmt.Println(msg)
15    fmt.Println("End of Program")
16}
17
18func hello(ch chan string) {
19	ch <- "Hello"
20}

This code declares a channel ch of type chan string. The next line creates this variable using make() function. The goroutine hello is passed reference to the channel and inside this routine, it sends a message "Hello" to the channel. In the main() function, we receive this data from the channel in msg variable which is printed when we run this code.

The data sent in channel can be received only once in any of the goroutines. By default, when we created channel, it creates unbuffered channel. Earlier, we need time.Sleep() or WaitGroup in order to make sure that we allow other goroutines to finish before main goroutine. However, in the output, notice that the main goroutine waits until you’ve read from the channel. This means it’s going to block the execution of goroutine sending the data until the other go routine has received previous value. If you add a delay in hello goroutine, this can be easily seen that main goroutine is waiting for hello routine to finish.

1func hello(ch chan string) {
2	time.Sleep(2 * time.Second)
3	ch <- "Hello"
4}

This feature allows us to write concurrent programs. For example, below code crunches numbers in parallel where it can where it’s finding sum and max value from a slice.

 1package main
 2
 3import (
 4	"fmt"
 5	"time"
 6)
 7
 8func main() {
 9	fmt.Println("Main routine")
10	numbers := []int{1, 2, 12, 4, 5, 6, 7, 8}
11	start := time.Now()
12
13
14	ch1 := make(chan int)
15	go sum(numbers, ch1)
16	sum := <-ch1
17	fmt.Println("Sum:", sum)
18
19	ch2 := make(chan int)
20	go max(numbers, ch2)
21	max := <-ch2
22	fmt.Println("Max:", max)
23
24	fmt.Println("Time taken:", time.Since(start))
25}
26
27func sum(numbers []int, ch chan int) {
28	sum := 0
29	for _, number := range numbers {
30		sum += number
31	}
32	time.Sleep(2 * time.Second)
33	ch <- sum
34}
35
36func max(numbers []int, ch chan int) {
37	max := numbers[0]
38	for _, number := range numbers {
39		if number > max {
40			max = number
41		}
42	}
43	time.Sleep(2 * time.Second)
44	ch <- max
45}

Here, channel ch1 is created of type chan int and passed to sum function where it takes 2 seconds. Notice that the execution is blocking, so it takes 2 seconds. The program waits for message to be written to channel. Similarly new channel ch2 is created and used for calculating max from an array which also takes 2 seconds. In total, this code execution takes about 4 seconds.

go run main.go
Main routine
Sum: 45
Max: 12
Time taken: 4.002665167s

The code actually waits where we are reading from the channel, so if I move my read statements together, I might be able to get some parallelism like below snippet for main function.

 1func main() {
 2	fmt.Println("Main routine")
 3	numbers := []int{1, 2, 12, 4, 5, 6, 7, 8}
 4	start := time.Now()
 5
 6
 7	ch1 := make(chan int)
 8	go sum(numbers, ch1)
 9
10	ch2 := make(chan int)
11	go max(numbers, ch2)
12	sum := <-ch1
13	fmt.Println("Sum:", sum)
14	max := <-ch2
15	fmt.Println("Max:", max)
16
17	fmt.Println("Time taken:", time.Since(start))
18}

This code execution takes only 2 seconds.

Unbuffered Channel

When we created channels above, they were by default unbuffered channels. This means it will block the execution of go routine sending the data until the other go routine has received the previously sent data. The receiver on a channel is blocked until there is some data coming in from another go routine into that channel. This type of channels are called Unbuffered channels. Tehy need a receiver as soon as a message is emitted to the channel. They do not have buffer which means they do not have capacity to hold on to data so we don’t have to specify capacity of the channel when creating a channel using make function. They do not store data, so they do not have any length.

For unbuffered channels, the length and the capacity is always zero.

Buffered Channels

On the other hand, if we want channel to hold some data, we can define a buffered channel. These types of channel do not block the execution until the buffer is full and at that point sending data into this channel is blocked until data is received by another goroutine. Similarly, receiver is blocked if the channel is totally empty. To create a buffered channel, we can define the capacity in the make function using the syntax below.

1ch := make(chan <data_type>, capacity)
2ch := make(chan int, 10)

Again, to find the capacity of the channel, we can use cap() function and to find currently available data in the channel, we can use len() function. The len() function will represent number of elements queued in this channel. Based on this, the length of the channel can never be higher than the capacity of the channel.

 1package main
 2
 3import (
 4	"fmt"
 5	"sync"
 6)
 7
 8func main() {
 9	ch := make(chan int, 3)
10	var wg sync.WaitGroup
11	wg.Add(2)
12	go send(ch, &wg)
13	wg.Wait()
14}
15
16func send(ch chan int, wg *sync.WaitGroup) {
17	fmt.Println("Inside send go-routine")
18	ch <- 1
19	ch <- 2
20	ch <- 3
21	go receive(ch, wg)
22	fmt.Println("Sent 3 elements to the channel")
23	wg.Done()
24}
25
26func receive(ch chan int, wg *sync.WaitGroup) {
27	fmt.Println("Waiting for incoming data")
28	fmt.Println("Received:", <- ch)
29	wg.Done()
30}

In this code, we are creating a buffered channel of capacity three. We are adding 2 to wait group and sending 3 elements to the channel. With this, we can receive the incoming data from the channel using receive function.

go run main.go
Inside send go-routine
Sent 3 elements to the channel
Waiting for incoming data
Received: 1

Again, if we call receive before sending any data into the channel, we run into deadlock error because channel is empty and we are trying to read from the channel. Below snippet shows demonstration where it throws deadlock error.

1func send(ch chan int, wg *sync.WaitGroup) {
2	fmt.Println("Inside send go-routine")
3	go receive(ch, wg)
4	fmt.Println("Sent 3 elements to the channel")
5	wg.Done()
6}

Also, because the channel has size of three, we cannot send forth element until we have read previous elements. So, below code also throws deadlock error.

 1func send(ch chan int, wg *sync.WaitGroup) {
 2	fmt.Println("Inside send go-routine")
 3	ch <- 1
 4	ch <- 2
 5	ch <- 3
 6	ch <- 4
 7	go receive(ch, wg)
 8	fmt.Println("Sent 3 elements to the channel")
 9	wg.Done()
10}

To avoid this error, we have to make sure that we do not write more than the capacity of the channel. If we want to write more data, we first have to read from the channel like below snippet.

 1func send(ch chan int, wg *sync.WaitGroup) {
 2	fmt.Println("Inside send go-routine")
 3	ch <- 1
 4	ch <- 2
 5	ch <- 3
 6	go receive(ch, wg)
 7	ch <- 4
 8	go receive(ch, wg)
 9	fmt.Println("Sent 3 elements to the channel")
10	wg.Done()
11}

Below code shows produce consume kind of pattern with the help of goroutines and channels.

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8func main() {
 9    var wg sync.WaitGroup
10    wg.Add(2)
11    ch := make(chan int, 10)
12    go produce(ch, &wg)
13    wg.Wait()
14}
15
16func produce(ch chan int, wg *sync.WaitGroup) {
17    for i := 10; i <= 100; i += 10 {
18        ch <- i
19    }
20    fmt.Println("Exiting Produce")
21    close(ch)
22    go consume(ch, wg)
23    wg.Done()
24}
25
26func consume(ch chan int, wg *sync.WaitGroup) {
27    for val := range ch {
28        fmt.Println("Received: ", val)
29    }
30    fmt.Println("Exiting Consume")
31    wg.Done()
32}

While receiving data from the channel, we can check if the channel is closed by assigning second value when receiving from the channel. This will have boolean values. If ok has true, it means channel is open and if ok is false, channel is closed and we will not receive any more values.

1v, ok := <- ch
 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	ch := make(chan int, 10)
 7	ch <- 1
 8	ch <- 2
 9
10	data, ok := <-ch
11	fmt.Println(data, ok)
12	close(ch)
13	data, ok = <-ch
14	fmt.Println(data, ok)
15	data, ok = <-ch
16	fmt.Println(data, ok)
17}
go run main.go
1 true
2 true
0 false

Closing a Channel

To close a channel, we can use close() function. We need to close the channel to make sure no more data can be sent to the channel. This is done when we do not want to send any more data to the channel.

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	ch := make(chan int, 10)
 7	ch <- 1
 8	ch <- 2
 9	close(ch)
10
11	data, ok := <-ch
12	fmt.Println(data, ok)
13	data, ok = <-ch
14	fmt.Println(data, ok)
15	data, ok = <-ch
16	fmt.Println(data, ok)
17}

In this example, we write data into the channel. After that, we close the channel using close function. We can still read the data using channel read operation.

Iterating Channel Data

To iterate through channel data we can use for ... range structure. In this case, we have to ensure that we close the channel.

 1package main
 2
 3import (
 4	"fmt"
 5	"sync"
 6)
 7
 8func main() {
 9	var wg sync.WaitGroup
10	ch := make(chan int, 3)
11	wg.Add(1)
12	go assign(ch, &wg)
13	wg.Add(1)
14	go work(ch, &wg)
15	wg.Wait()
16}
17
18func assign(ch chan int, wg *sync.WaitGroup) {
19	ch <- 1
20	ch <- 2
21	ch <- 3
22	fmt.Println("Assigned work to worker")
23	close(ch)
24	wg.Done()
25}
26
27func work(ch chan int, wg *sync.WaitGroup) {
28	fmt.Println("Waiting for some work")
29	for value := range ch {
30		fmt.Println("Received:", value)
31	}
32	wg.Done()
33}