Channels in Go


It’s been some time since I’ve been using Go as the main language for backend development. Whenever I meet someone and they ask me what is the special thing about Go? I always have many things to say about that and definitely channels is among them. So if you’re like me, who started multithreaded programming in C or any other language, and know how tricky it can be to communicate and synchronise between the threads for writing concurrent programs using sockets, pipes, and mutexes, you’re in for a treat 😍

Goroutines

Before we see what channels are, let’s see what a goroutine is:

It’s an independently executing function, launched by a go statement.

A goroutine is a function when executed runs independently of the main function and the main function doesn’t wait for the function to be completed. This can help manage a lot of things at once, which is the definition of concurrency.

We can visualise this by thinking of a server which listens for requests from clients and creates new goroutines for each request it gets.

    while true:
        listening for requests
        if got new request:
            go createASeparteGoroutineForNewRequest()

Creating goroutines is very cheap and you can create thousands of goroutines because they have their own stack which adjusts according to requirement. But you need to keep in mind that goroutine is not a thread. There can be thousands of goroutines running on the same thread dynamically. But for the sake of understanding, we think of goroutoine as an extremely cheap thread.

But as in the real world, different processes don’t just run independently but have some sort of communication to reach a common goal to solve any problem. We also need to communicate between goroutines and that’s where channels come into play.

Channels

A channel in Go provides a connection between the goroutines, allowing them to communicate.

When declaring a channel, the type of data that will be shared needs to be specified. Values and pointers of built-in, named, struct, and reference types can be shared through a channel.

Creating a channel in Go requires the use of the built-in function make.

    // Declaring and initializing
    var c chan int
    c = make(chan int)
    // or
    c:= make(chan int)

On channel you can either send or receive a value.

In order to send or receive a value on the channel we use the following notations:

    // Sending on the channel
    c  <- 1
    // Receiving on the channel
    value = <- c

Channels can be of two types, unbuffered and buffered channels. Here we will only talk about the unbuffered channels.

An unbuffered channel is a channel with no capacity to hold any value before it’s received.

The send and receive channel in an unbuffered channel is a blocking operation. This means that both send and recieve will wait for each other to be completed.

Let’s see an example here:

A channel is used for the communication between the main function and a goroutine.

Example: 1

func main() {
    c := make(chan string)
    go gopher("Hey gopher!", c)
    for i := 0; i < 6; i++ {
        fmt.Printf("You say: %q\n", <-c) // Receive expression
    }
    fmt.Println("Hey gopher; I'm leaving.")
}
func gopher(msg string, c chan string) {
    for i := 0; ; i++ {
        c <- fmt.Sprintf("%s %d", msg, i) // send expression
        time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
    }
}

Output:

You say: "Hey gopher! 0"
You say: "Hey gopher! 1"
You say: "Hey gopher! 2"
You say: "Hey gopher! 3"
You say: "Hey gopher! 4"
You say: "Hey gopher! 5"
Hey gopher; I'm leaving.

The receive expression in main function waits for the send expression in the goroutine to send a value. And once the value is sent in the channel, it will wait until the value has been received in the main function. Notice that I said that send will wait even after sending the value in the channel until the receive has received the value.

So as you noticed we achieved two things here, one is the communication(passing of value between goroutines) and the second, synchronization, the order in which the goroutines will move forward, depending upon the data they are receiving.

Tennis game simulation from “Go in Action” book

let’s look at another example that uses an unbuffered channel to synchronize the exchange of data between two goroutines.

Example: 2

In the game of tennis, two players hit a ball back and forth against each other. The players are always in one of two states: either waiting to receive the ball or sending the ball back to the opposing player.

tennis game

You can simulate a game of tennis using two goroutines and an unbuffered channel to simulate the exchange of the ball.

01 // This sample program demonstrates how to use an unbuffered
02 // channel to simulate a game of tennis between two goroutines.
03 package main
04
05 import (
06      "fmt"
07      "math/rand"
08      "sync"
09      "time"
10 )
11
12  // wg is used to wait for the program to finish.
13  var wg sync.WaitGroup
14
15  func init() {
16      rand.Seed(time.Now().UnixNano())
17 }
18
19  // main is the entry point for all Go programs.
20  func main() {
21      // Create an unbuffered channel.
22      court := make(chan int)
23
24      // Add a count of two, one for each goroutine.
25      wg.Add(2)
26
27      // Launch two players.
28      go player("Nadal", court)
29      go player("Djokovic", court)
30
31      // Start the set.
32      court <- 1
33
34      // Wait for the game to finish.
35      wg.Wait()
36  } 
37
38  // player simulates a person playing the game of tennis.
39  func player(name string, court chan int) {
40       // Schedule the call to Done to tell main we are done.
41       defer wg.Done()
42
43      for {
44          // Wait for the ball to be hit back to us.
45          ball, ok := <-court
46          if !ok {
47              // If the channel was closed we won.
48              fmt.Printf("Player %s Won\n", name)
49              return
50          }
51
52          // Pick a random number and see if we miss the ball.
53          n := rand.Intn(100)
54          if n%13 == 0 {
55              fmt.Printf("Player %s Missed\n", name)
56
57              // Close the channel to signal we lost.
58              close(court)
59              return
60          }
61
62          // Display and then increment the hit count by one.
63          fmt.Printf("Player %s Hit %d\n", name, ball)
64          ball++
65
66          // Hit the ball back to the opposing player.
67          court <- ball
68      }
69  }

​ Original listing on Github: tennis game simulation

Output
Player Nadal Hit 1
Player Djokovic Hit 2
Player Nadal Hit 3
Player Djokovic Missed
Player Nadal Won

Explanation

  • On line 43, an unbuffered channel of type int is created in the main function for synchronizing the exchange of the ball between two players(goroutines)
  • On lines 28 and 29 two goroutines are created which will take part in the play
  • At this point, both goroutines are locked waiting to receive the ball
  • On line 32, a ball is sent into the channel and the game is played until one of the goroutines lose
  • On line 43, you’ll find an infinite loop inside which the game is played
  • On line 45, the goroutine performs a receive on the channel, waiting to receive the ball. This locks the goroutine until a send is performed on the channel.
  • On line 46, the ok flag is checked for false once the receive on the channel returns. A value of false indicates the channel was closed and the game is over.
  • On lines 53-60, a random number is generated to determine if the goroutine hits or misses the ball.
  • On line 64, the value of the ball is incremented by 1 if the ball was hit and the ball is sent back to the player on line 67
  • At this point, both goroutines are locked until the exchange is made
  • Eventually, a goroutine misses the ball and the channel is closed on line 58
  • Then both goroutines return, the call to Done via the defer statement is performed, and the program terminates.

The flow of the code was in line with the way these events and activities take place in the real world. This makes the code readable and self-documenting.

Conclusion

So as you saw, channels make our lives so much easier when it comes to writing concurrent programs. Go has many more exceptional features that make it a fun language to work with. I am sure you’ll be interested to know what other interesting things Go has to offer, you can check out Go Docs and keep an eye out on our Geek Space 9 blog because we will be uploading new blogs about new and cool features of Go.

References:

  1. Rob Pike: Go Concurrency Patterns
  2. Go in Action book by William Kennedy, Brian Ketelsen, Erik St. Martin