Channels

Channels are the second primitive in the language that allows us to write concurrent applications. We have talked a bit about channels in the Communicating sequential processes section.

Channels are the way we communicate between processes. We could be sharing a memory location and using mutexes to control the processes' access. But channels provide us with a more natural way to handle concurrent applications that also produces better concurrent designs in our programs.

Our first channel

Working with many Goroutines seems pretty difficult if we can't create some synchronization between them. The order of execution could be irrelevant as soon as they are synchronized. Channels are the second key feature to write concurrent applications in Go.

A TV channel in real life is something that connects an emission (from a studio) to millions of TVs (the receivers). Channels in Go work in a similar fashion. One or more Goroutines can work as emitters, and one or more Goroutine can act as receivers.

One more thing channels, by default, block the execution of Goroutines until something is received. It is as if our favourite TV show delays the emission until we turn the TV on so we don't miss anything.

How is this done in Go?

package main 
 
import "fmt" 
 
func main() { 
  channel := make(chan string) 
  go func() { 
    channel <- "Hello World!" 
  }() 
 
  message := <-channel 
  fmt.Println(message) 
} 

To create channels in Go, we use the same syntax that we use to create slices. The make keyword is used to create a channel, and we have to pass the keyword chan and the type that the channel will transport, in this case, strings. With this, we have a blocking channel with the name channel. Next, we launch a Goroutines that sends the message Hello World! to the channel. This is indicated by the intuitive arrow that shows the flow--the Hello World! text going to (<-) a channel. This works like an assignment in a variable, so we can only pass something to a channel by first writing the channel, then the arrow, and finally the value to pass. We cannot write "Hello World!" -> channel.

As we mentioned earlier, this channel is blocking the execution of Gorountines until a message is received. In this case, the execution of the main function is stopped until the message from the launched Goroutines reaches the other end of the channel in the line message := <-channel. In this case, the arrow points in the same direction, but it's placed before the channel, indicating that the data is being extracted from the channel and assigned to a new variable called message (using the new assignment ":=" operator).

In this case, we don't need to use a WaitGroup to synchronize the main function with the created Goroutines, as the default nature of channels is to block until data is received. But does it work the other way around? If there is no receiver when the Goroutine sends the message, does it continue? Let's edit this example to see this:

package main 
 
import ( 
  "fmt" 
  "time" 
) 
 
func main() { 
  channel := make(chan string) 
 
  var waitGroup sync.WaitGroup 
 
  waitGroup.Add(1) 
  go func() { 
    channel <- "Hello World!" 
    println("Finishing goroutine") 
    waitGroup.Done() 
  }() 
 
  time.Sleep(time.Second) 
  message := <-channel 
  fmt.Println(message) 
  waitGroup.Wait() 
} 

We are going to use the Sleep function again. In this case, we print a message when the Goroutine is finished. The big difference is in the main function. Now we wait one second before we listen to the channel for data:

$ go run main.go

Finishing goroutine
Hello World!

The output can differ because, again, there are no guarantees in the order of execution, but now we can see that no message is printed until one second has passed. After the initial delay, we start listening to the channel, take the data, and print it. So the emitter also has to wait for a cue from the other side of the channel to continue its execution.

To recap, channels are ways to communicate between Goroutines by sending data through one end and receiving it at the other (like a pipe). In their default state, an emitter Goroutine will block its execution until a receiver Goroutine takes the data. The same goes for a receiver Goroutine, which will block until some emitter sends data through the channel. So you can have passive listeners (waiting for data) or passive emitters (waiting for listeners).

Buffered channels

A buffered channel works in a similar way to default unbuffered channels. You also pass and take values from them by using the arrows, but, unlike unbuffered channels, senders don't need to wait until some Goroutine picks the data that they are sending:

package main 
 
import ( 
  "fmt" 
  "time" 
) 
 
func main() { 
  channel := make(chan string, 1) 
 
  go func() { 
    channel <- "Hello World!" 
    println("Finishing goroutine") 
  }() 
 
  time.Sleep(time.Second) 
 
  message := <-channel 
  fmt.Println(message) 
} 

This example is like the first example we used for channels, but now we have set the capacity of the channel to one in the make statement. With this, we tell the compiler that this channel has a capacity of one string before getting blocked. So the first string doesn't block the emitter, but the second would. Let's run this example:

$ go run main.go

Finishing goroutine
Hello World!

Now we can run this small program as many times as we want--the output will always be in the same order. This time, we have launched the concurrent function and waited for one second. Previously, the anonymous function wouldn't continue until the second has passed and someone can pick the sent data. In this case, with a buffered channel, the data is held in the channel and frees the Goroutine to continue its execution. In this case, the Goroutine is always finishing before the wait time passes.

This new channel has a size of one, so a second message would block the Goroutine execution:

package main 
 
import ( 
  "fmt" 
  "time" 
) 
 
func main() { 
  channel := make(chan string, 1) 
 
  go func() { 
    channel <- "Hello World! 1" 
    channel <- "Hello World! 2" 
    println("Finishing goroutine") 
  }() 
 
  time.Sleep(time.Second) 
 
  message := <-channel 
  fmt.Println(message) 
} 

Here, we add a second Hello world! 2 message, and we provide it with an index. In this case, the output of this program could be like the following:

$ go run main.go
Hello World! 1

Indicating that we have just taken one message from the channel buffer, we have printed it, and the main function finished before the launched Goroutine could finish. The Goroutine got blocked when sending the second message and couldn't continue until the other end took the first message. Then it prints it so quickly that it doesn't have time to print the message to show the ending of the Goroutine. If you keep executing the program on the console, sooner or later the scheduler will finish the Goroutine execution before the main thread.

Directional channels

One cool feature about Go channels is that, when we use them as parameters, we can restrict their directionality so that they can be used only to send or to receive. The compiler will complain if a channel is used in the restricted direction. This feature applies a new level of static typing to Go apps and makes code more understandable and more readable.

We'll take a simple example with channels:

package main 
 
import ( 
  "fmt" 
  "time" 
) 
 
func main() { 
  channel := make(chan string, 1) 
 
  go func(ch chan<- string) { 
    ch <- "Hello World!" 
    println("Finishing goroutine") 
  }(channel) 
 
  time.Sleep(time.Second) 
 
  message := <-channel 
  fmt.Println(message) 
} 

The line where we launch the new Goroutine go func(ch chan<- string) states that the channel passed to this function can only be used as an input channel, and you can't listen to it.

We can also pass a channel that will be used as a receiver channel only:

func receivingCh(ch <-chan string) { 
  msg := <-ch 
  println(msg) 
} 

As you can see, the arrow is on the opposite side of the keyword chan, indicating an extracting operation from the channel. Keep in mind that the channel arrow always points left, to indicate a receiving channel, it must go on the left, and to indicate an inserting channel, it must go on the right.

If we try to send a value through this receive only channel, the compiler will complain about it:

func receivingCh(ch <-chan string) { 
  msg := <-ch 
  println(msg) 
  ch <- "hello" 
} 

This function has a receive only channel that we will try to use to send the message hello through. Let's see what the compiler says:

$ go run main.go
./main.go:20: invalid operation: ch <- "hello2" (send to receive-only type <-chan string)

It doesn't like it and asks us to correct it. Now the code is even more readable and safe, and we have just placed an arrow in front or behind the chan argument.

The select statement

The select statement is also a key feature in Go. It is used to handle more than one channel input within a Goroutine. In fact, it opens lots of possibilities, and we will use it extensively in the following chapters.

The select statement

In the select structure, we ask the program to choose between one or more channels to receive their data. We can save this data in a variable and make something with it before finishing the select. The select structure is just executed once; it doesn't matter if it is listening to more channels, it will be executed only once and the code will continue executing. If we want it to handle the same channels more than once, we have to put it in a for loop.

We will make a small app that will send the message hello and the message goodbye to the same Goroutine, which will print them and exit if it doesn't receive anything else in five seconds.

First, we will make a generic function that sends a string over a channel:

func sendString(ch chan<- string, s string) { 
  ch <- s 
} 

Now we can send a string over a channel by simply calling the sendString method. It's time for the receiver. The receiver will take messages from both channels--the one that sends hello messages and the one that sends goodbye messages. You can also see this in the previous diagram:

func receiver(helloCh, goodbyeCh <-chan string, quitCh chan<- bool) { 
  for { 
    select { 
    case msg := <-helloCh: 
      println(msg) 
    case msg := <-goodbyeCh: 
      println(msg) 
    case <-time.After(time.Second * 2): 
      println("Nothing received in 2 seconds. Exiting") 
      quitCh <- true 
      break 
    } 
  } 
} 

Let's start with the arguments. This function takes three channels--two receiving channels and one to send something through it. Then, it starts an infinite loop with the for keyword. This way we can keep listening to both channels forever.

Inside the scope of select block, we have to use a case for each channel we want to handle (have you realized how similar it is to the switch statement?). Let's see the three cases step by step:

  • The first case takes the incoming data from the helloCh argument and saves it in a variable called msg. Then it prints the contents of this variable.
  • The second case takes the incoming data from the goodbyeCh argument and saves it in a variable called msg too. Then it also prints the content of this variable.
  • The third case is quite interesting. It calls the time function. After that, if we check its signature, it accepts a time and duration value and returns a receiving channel. This receiving channel will receive a time, the value of time after the specified duration has passed. In our example, we use the channel it returns as a timeout. Because the select is restarted after each handle, the timer is restarted too. This is a very simple way to set a timer to a Goroutine waiting for the response of one or many channels.

Everything is ready for the main function:

package main 
import "time" 
 
func main() { 
  helloCh := make(chan string, 1) 
  goodbyeCh := make(chan string, 1) 
  quitCh := make(chan bool) 
  go receiver(helloCh, goodbyeCh, quitCh) 
 
  go sendString(helloCh, "hello!") 
 
  time.Sleep(time.Second) 
 
  go sendString(goodbyeCh, "goodbye!") 
  <-quitCh 
} 

Again, step by step, we created the three channels that we'll need in this exercise. Then, we launched our receiver function in a different Goroutine. This Goroutine is handled by Go's scheduler and our program continues. We launched a new Goroutine to send the message hello to the helloCh arguments. Again, this is going to occur eventually when the Go's scheduler decides.

Our program continues again and waits for a second. In this break, Go's scheduler will have time to execute the receiver and the first message (if it hasn't done so yet), so the hello! message will appear on the console during the break.

A new message is sent over the goodbye channel with the goodbye! text in a new Goroutine, and our program continues again to a line where we wait for an incoming message in the quitCh argument.

We have launched three Goroutines already--the receiver that it is still running, the first message that had finished when the message was handled by the select statement, and the second message was been printed almost immediately and had finished too. So just the receiver is running at this moment, and if it doesn't receive any other message in the following two seconds, it will handle the incoming message from the time structure. After channel type, print a message to say that it is quitting, send a true to the quitCh, and break the infinite loop where it was looping.

Let's run this small app:

$ go run main.go

hello!
goodbye!
Nothing received in 2 seconds. Exiting

The result  may not be very impressive, but the concept is clear. We can handle many incoming channels in the same Goroutine by using the select statement.

Ranging over channels too!

The last feature about channels that we will see is ranging over channels. We are talking about the range keyword. We have used it extensively to range over lists, and we can use it to range over a channel too:

package main 
 
import "time" 
 
func main() { 
  ch := make(chan int) 
 
  go func() { 
    ch <- 1 
    time.Sleep(time.Second) 
 
    ch <- 2 
 
    close(ch) 
  }() 
  for v := range ch { 
    println(v) 
  } 
} 

In this case, we have created an unbuffered channel, but it would work with a buffered one too. We launched a function in a new Goroutine that sends the number "1" over a channel, waits a second, sends the number "2", and closes the channel.

The last step is to range over the channel. The syntax is quite similar to a list range. We store the incoming data from the channel in the variable v and we print this variable to the console. The range keeps iterating until the channel is closed, taking data from the channel.

Can you guess the output of this little program?

$ go run main.go

1
2

Again, not very impressive. It prints the number "1", then waits a second, prints the number "2", and exits the application.

According to the design of this concurrent app, the range was iterates over possible incoming data from the

channel

until the concurrent Goroutine closes this channel. At that moment, the range finishes and the app can exit.

Range is very useful in taking data from a channel, and it's commonly used in fan-in patterns where many different Goroutines send data to the same channel.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.103.221