Getting started with GO Channels
In Go programming, you may want goroutines to communicate with one another. Go channels are responsible for allowing that kind of communication to take place. Therefore, go channels are conduit pipes that are typed through which values are sent and received. On the other hand, goroutines are light weight threads that are managed by the main go thread. For information about goroutine, visit the goroutine tutorial. Go has two types of channels that we will discuss later in this tutorial. They are buffered and unbuffered channels.
Why use channels in golang?
- Go channels make asynchronous programming easy and fun to use.
- Channels provide mechanisms for concurrent execution of functions to communicate by sending and receiving values of a specific element type.
Creating a Go channel
It is easy to create Go channels. You only need to define a channel, channel type using the make function and assign that to a variable. Just like any other types like structs, maps and slices, you need to create a channel before you can use it as shown below.
Syntax
package main
func main() {
myChannel := make(chan datatype)
}
Here is a more descriptive syntax to create a go channel
How channels work (sending and receiving data)?
In Go channels, it is very easy to send and receive values to and from a Go channel. Go provides an operator used to show the direction of flow to and from a channel. This operator is <-
, a less than and minus sign. It is more like an arrow that will always point to the direction of value flow. Below is an example of sending a value into a channel.
myChannel <- "Hello world"
In the above example, we are sending the string “Hello world” string into the mychannel
channels. It is worth mentioning that the channel only accepts values that are of the same type as the channel.We can also receive values from a channel and store the value in a variable as shown below.
value := <-myChannel
In the above example, the arrow points away from the channel, to indicate that messages are coming from the channel.
Example
package main
import "fmt"
func main() {
linkChannel := make(chan string)
go func() {
links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
for _, link := range links {
linkChannel <- link
}
}()
link1 := <-linkChannel
link2 := <-linkChannel
link3 := <-linkChannel
fmt.Println(link1)
fmt.Println(link2)
fmt.Println(link3)
}
Explanation
In the above example, we define a go channel of type string
using the line linkChannel := make(chan string)
. Values of string type are the only acceptable values that can go through the channel. We then define an anonymous goroutine that loops through an array of links. For each loop we send a link into the channel using linkChannel <- link
. The goroutine will run in the background simultaneously with the main thread.
One important thing to note is that, the main thread(main goroutine) has to wait for messages to come from the channel. This means that receiving values from channels is blocking. Channels will allow messages to go through it only when the receiver is ready to receive messages from it.
Output
$ go run main.go
https://www.golinuxcloud.com/
https://www.tesla.com/
https://www.google.com/
Properly closing a channel
Channels support a third operation, close, which sets a flag indicating that no more values will ever be sent on this channel; subsequent attempts to send will panic. Receive operations on a closed channel yield the values that have been sent until no more values are left; any receive operations thereafter complete immediately and yield the zero value of the channel’s element type.
To close a channel, we call the built-in close function:
close(ch)
Example
package main
import "fmt"
func main() {
linkChannel := make(chan string)
go ping(linkChannel)
link1 := <-linkChannel
link2 := <-linkChannel
link3 := <-linkChannel
fmt.Println(link1)
fmt.Println(link2)
fmt.Println(link3)
}
func ping(c chan string) {
links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
for _, link := range links {
c <- link
}
close(c)
}
Explanation
We define a ping()
function that runs as a goroutine. ping()
function takes a channel of string as an argument. It loops through an array of links and sends these links to the channel. When the loop is done, we exit the for loop and close the channel using the close()
syntax. Closing a channel means no more values can be sent into the channel.An attempt to send values into a closed channel will result in a panic with messages that say “send on closed channel”.
Output
$ go run main.go
https://www.golinuxcloud.com/
https://www.tesla.com/
https://www.google.com/
How to check if channel is closed?
It's possible to check if a channel is closed or not using the below syntax.
package main
import "fmt"
func main() {
linkChannel := make(chan string)
go ping(linkChannel)
for {
_, ok := <-linkChannel
if ok {
fmt.Println("Channel Open")
} else {
fmt.Println("Channels closed")
break
}
}
}
func ping(c chan string) {
links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
for _, link := range links {
c <- link
}
close(c)
}
Explanation
In the above example, we check if a channel is closed or is open, using the syntax _, ok := <-linkChannel
. We really don't care about the first returned value , therefore we use the underscore. The ok
variable is a boolean type and turns true
when the channel is open and turns false
when the channel is closed.
Output
$ go run main.go
Channel Open
Channel Open
Channel Open
Channels closed
Different types of go channels
Unbuffered channels
Unbuffered channels are channels that can only accept sending (chan <-
) of values through them only when we have a corresponding receiver (<- chan
) that will receive the sent values. Here we have two functions — sendData()
and getData()
:
package main
import (
"fmt"
"time"
)
// ---send data into a channel---
func sendData(ch chan string) {
fmt.Println("Sending a string into channel…")
ch <- "Hello"
fmt.Println("String has been retrieved from channel…")
}
// ---getting data from the channel---
func getData(ch chan string) {
time.Sleep(2 * time.Second)
fmt.Println("String retrieved from channel:", <-ch)
}
func main() {
ch := make(chan string)
go sendData(ch)
go getData(ch)
fmt.Scanln()
}
Explanation:
In the main()
function, you first create a channel using the make()
function, ch
, with the type of channel specified (string). This means that the channel can only contain values of type string. You then call the sendData()
and getData()
functions as goroutines. In the sendData()
function, you first print out the sentence Sending a string into channel…
At the same time, we're also running the getData()
function. Here, with getData()
, you're trying to receive a value from the channel. Because there is currently no value in the channel (it won’t have any value in it until two seconds later), the getData()
function will block. The moment a value is available in the channel, the getData()
function will unblock and retrieve the value from the channel.
In the sendData()
function, you try to print a sentence immediately after sending a value into the channel. In the getData()
function, you insert a two-second delay before you retrieve the value from the channel.
Output:
$ go run main.go
Sending a string into channel…
String retrieved from channel: Hello
String has been retrieved from channel…
Buffered channels
On the other hand, buffered channels only accept a limited number of values without a corresponding receiver of those values. Provide the buffer size/length/capacity as the second argument to the make function to initialize a buffered channel. Buffered channels can only block when the buffer is full. Therefore sending on a buffered channel will block the channel when the channel is full and the receiver will block when the buffer is empty.
Buffered Channel Syntax
The size of the buffer is specified as an argument to the make function
Example
package main
import (
"fmt"
)
func main() {
linkChannel := make(chan string, 5)
go ping(linkChannel)
fmt.Println(<-linkChannel)
fmt.Println(<-linkChannel)
fmt.Println(<-linkChannel)
}
func ping(c chan string) {
links := []string{"https://www.golinuxcloud.com/", "https://www.tesla.com/", "https://www.google.com/"}
for _, link := range links {
c <- link
}
}
Explanation
In the above example we define a goroutine that loops through links and sends them to the buffered linkChannnel
channel. In the main thread(main routine), we receive the values(fmt.Println(<-linkChannel
)) being sent from the ping()
goroutine and print the values out in the terminal.
Please note that buffered channels will panic or return an error whenever you try to add more values than the buffer size. Also the receiver will cause a panic if we try to receive(<-linkChannel
) from an empty channel.
Output
$ go run main.go
https://www.golinuxcloud.com/
https://www.tesla.com/
https://www.google.com/
How different channels work?
Channels have many different states, each with a different behavior when reading, writing, or closing.
Unbuffered, open | Unbuffered, closed | Buffered, open | Buffered, closed | Nil | |
---|---|---|---|---|---|
Read | Pause until something is written | Return zero value (use comma ok to see if closed) | Pause if buffer is empty | Return a remaining value in the buffer. If the buffer is empty, return zero value (use comma ok to see if closed) | Hang forever |
Write | Pause until something is read | PANIC | Pause if buffer is full | PANIC | Hang forever |
Close | Works | PANIC | Works, remaining values still there | PANIC | PANIC |
Go channel select statement
The select statement in Go channels is used to wait on multiple channel operations like receiving and sending values. The select statement waits for simultaneous send and receive operations. Below are the characteristics of a select statement working with channels.
select
statement is a blocking statement. This means that one case in the select statement will have to wait until one of the operations becomes unblocked.- If several cases can proceed, a single one case will be chosen to execute at random.
- The default case will be executed if all cases are blocked.
Select statement syntax
select {
case <-ch1:
fmt.Println("Getting data from channel one")
case <-ch2:
fmt.Println("Getting data from channel two")
default:
fmt.Println("Run if all cases are blocked")
}
}
Example
package main
import (
"fmt"
"time"
)
func goroutine1(ch1 chan string) {
time.Sleep(time.Second)
for i := 0; i < 3; i++ {
ch1 <- fmt.Sprintf("%d ==> Channel 1 message", i)
}
}
func goroutine2(ch2 chan string) {
for i := 0; i < 3; i++ {
ch2 <- fmt.Sprintf("%d ==> Channel 2 message", i)
}
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go goroutine1(ch1)
go goroutine2(ch2)
for {
time.Sleep(time.Second * 3)
select {
case value1 := <-ch1:
fmt.Println(value1)
case value2 := <-ch2:
fmt.Println(value2)
default:
fmt.Println("All channels are blocking")
}
}
}
Explanation
In our select example above, we define two goroutines that loop through integers and send a string value to the channel after every iteration. The first goroutine1 sleeps for a second before executing while goroutine2 starts executing immediately after being called. This is to show an example of a function that takes much longer time than the other to run. We then declare and call two goroutines to start sending values into their respective channels. In order to have enough time to receive those values, we use an infinite loop that has the select statement
. The select statement sleeps for 3 seconds to give the channels time to start reporting value in the main thread. The select statement will then start executing cases depending on which channel is ready to receive values as shown in the output. Another thing to note is that the default case will be called when channels finish receiving values (start blocking).
Output
$ go run main.go
0 ==> Channel 1 message
0 ==> Channel 2 message
1 ==> Channel 1 message
1 ==> Channel 2 message
2 ==> Channel 1 message
2 ==> Channel 2 message
All channels are blocking
Some go channel examples
Example-1: How to count values in a channel
To get the number of values in a Go channel, use the built in function len()
.len()
returns the number of values in a buffered channel.
Example
package main
import "fmt"
func main() {
ch := make(chan int, 5)
for i := 0; i < 3; i++ {
ch <- i
}
fmt.Printf("Number of values in a channel: %d \n", len(ch))
}
Output
$ go run main.go
Number of values in a channel: 3
Example-2: Sending struct over a channel
Many examples in this tutorial, we have used Go channels for type string
and int
. In this section we will explore using channels of type struct
. Struct in a Go is a collection of fields, hence giving a chance to send and receive different data types across channels.
Example
package main
import "fmt"
type HTTPResponse struct {
statusCode int
body string
}
func ping(c chan HTTPResponse, responses []HTTPResponse) {
for i := 0; i < len(responses); i++ {
c <- responses[i]
}
close(c)
}
func main() {
response := []HTTPResponse{
{
statusCode: 200,
body: "Resource OK",
},
{
statusCode: 201,
body: "Resource Created",
},
{
statusCode: 404,
body: "Resource Not Found",
},
}
ch := make(chan HTTPResponse, 5)
go ping(ch, response)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Output
$ go run main.go
{200 Resource OK}
{201 Resource Created}
{404 Resource Not Found}
Example-3: Iterating through channels
We can use range keyword to iterate through a channel:
Example
package main
import (
"fmt"
"time"
)
func fib(n int, c chan int) {
a, b := 1, 1
for i := 0; i < n; i++ {
c <- a // blocked until value is received from channel
a, b = b, a+b
time.Sleep(1 * time.Second)
}
close(c) // close the channel
}
func main() {
c := make(chan int)
go fib(10, c)
for i := range c { // read from channel until channel is closed
fmt.Println(i)
}
}
Explanation:
In this example, you have a function named fib()
that takes in two arguments — the number of elements to generate for the Fibonacci sequence, and the channel to store the numbers in. Each number of the Fibonacci sequence is calculated and then sent into the channel. I've added a delay of one second for each number to simulate some delay. As each Fibonacci number is being generated and inserted into the channel, the fib()
function blocks until the value is retrieved from the channel. After generating all the required Fibonacci numbers, you close the channel (using the close()
function) to indicate that the channel is no longer accepting values.
In the main()
function, we first create an instance of the channel using the make()
function, and then proceed to call the fib()
function as a goroutine — generating the first ten Fibonacci numbers.
You use the range keyword on the c
channel to keep on reading values until the channel is closed.
Output:
$ go run main.go
1
1
2
3
5
8
13
21
34
55
Summary
Channels used together with goroutines are a powerful tool. Channels help developers to write goroutines that send and receive messages easily, hence making asynchronous programming easy and fun. In this tutorial we have explored what channels are and how they are being used.
References
goroutine concurrency
golang channel
Related Keywords: why use channels in golang, go channel select, go channel close, golang read from channel, golang count items in channel, golang channel blocking, golang channel types (unbuffered channel, bufferred channel), golang channel with multiple values, golang channel limit, golang send struct over channel, Why go channels are bad