The existence of channels really helps us to manage the goroutines that are running in our program. There are times when we also need to manage many goroutines and many channels are needed. So, this is where select
comes in handy. select
allows us to control data communication through channels. It is used in the same way as the switch
selection condition.
The following average and highest value lookup program is a simple example of the application of select in channels. There will be 2 goroutines each handled by a channel. Each time the goroutine finishes executing, it will send its data to the corresponding channel. Then, using select, we will control the reception of the data. First, we set up 2 functions that will be executed as new goroutines. The first function is used to find the average, and the second function is for determining the highest value of a slice.
Alright, we will try to implement a simple program to find the average and highest value of an array number. In this program, we will define 2 goroutines that each have a function that will be handled by a channel.
Every time the goroutine finishes executing, it will send the data to the channel concerned, then using select will control the reception of the data.
We first prepare 2 functions that will be executed as new goroutines, namely the first function to find the average and the second function to determine the highest value of an array slice
.
package main
func getAverage(numbers []int, ch chan float64) {
var sum = 0
for _, e := range numbers {
sum += e
}
ch <- float64(sum) / float64(len(numbers))
}
func getMax(numbers []int, ch chan int) {
var max = numbers[0]
for _, e := range numbers {
if max < e {
max = e
}
}
ch <- max
}
The two functions above will be executed in the main
function as a new goroutine. After that, both functions will send their data into the specified channel, which in this program we will distinguish the container variable. ch1
holds the average value, ch2
the highest value data result.
After that, let’s create the main
function below.
func main() {
runtime.GOMAXPROCS(2)
var numbers = []int{3, 4, 3, 5, 6, 3, 2, 2, 6, 3, 4, 6, 3}
fmt.Println("numbers: ", numbers)
var ch1 = make(chan float64)
go getAverage(numbers, ch1)
var ch2 = make(chan int)
go getMax(numbers, ch2)
for i := 0; i < 2; i++ {
select {
case avg := < ch1:
fmt.Printf("Avg \t: %.2f \n", avg)
case max := <-ch2:
fmt.Printf("Max \t: %d \n", max)
}
}
}
In the above code, the transaction of sending data on channels ch1 and ch2 is controlled using select. There are 2 case conditions for receiving data from both channels. The case condition avg := <-ch1 will be fulfilled when there is data reception from channel ch1, which will then be accommodated by the avg variable. The case condition max := <-ch2 will be fulfilled when there is data reception from channel ch2, which will then be accommodated by the max variable. Because there are 2 channels, it is necessary to prepare a loop 2 times before using the select keyword.
In the code above, sending data on channels ch1
and ch2
will be controlled by select
. There are 2 case conditions where the received data from both will be sent through the channel.
- The condition
case avg :=<-ch1
will be met when receiving data from channelch1
which will then be accommodated in the variableavg
. - The condition
case max := <-ch2
will be met when receiving data from channelch2
which will then be accommodated in the variablemax
.
Since there are 2 channels, we need to be prepared to loop twice before using the select
keyword.
➜ channel-select git:(main) ✗ go run main.go
numbers : [3 4 3 5 6 3 2 2 6 3 4 6 3]
Max: 6
Avg: 3.85
➜ channel-select git:(main) ✗ go run main.go
numbers : [3 4 3 5 6 3 2 2 6 3 4 6 3]
Avg: 3.85
Max: 6
Channel - Range and Close
Receiving data through channels that are used by multiple goroutines is made easier by utilizing the for - range
keyword. We can apply this to perform endless looping.
The loop continues even if there are no transactions on the channel and will only stop if the channel status changes to closed. The close
function is used to close the channel.
A channel that has been closed will not be able to be used again to receive and send data so that the for - range
will also stop.
Suppose we will make an implementation example as below.
We will create a sendMessage
function with a channel parameter which if the function is executed will loop 20 times and after all sent it will end with the close
channel.
func sendMessage(ch chan<- string) {
for i := 0; i < 20; i++ {
ch <- fmt.Sprintf("data %d", i)
}
close(ch)
}
Also set up a printMessage()
function to handle receiving data. Inside, the channel will be looped using for - range
and then the data will be displayed.
func printMessage(ch < channel string) {
for message := range ch {
fmt.Println(message)
}
}
After that we will create a new channel in the main function by running sendMessage()
as a goroutine and we will also run the printMessage
function. So, with this we will send 20 data through the new goroutine and will also be received by the goroutine as well.
func main() {
runtime.GOMAXPROCES(2)
var messages = make(chan string)
go sendMessage(messages)
printMessage(messages)
}
If the 20 data is successfully sent and received then the ch
channel will be shut down close
, so the looping of the channel data in printMessage()
will also stop.
➜ channel-range-close git:(main) ✗ go run main.go
data 0
data 1
data 2
data 3
data 4
data 5
data 6
data 7
data 8
data 9
data 10
data 11
data 12
data 13
data 14
data 15
data 16
data 17
data 18
data 19
Channel Direction
Golang is unique in the channel parameter feature that has been provided. The channel access level can be determined, whether we are only a receiver, sender or even both receiver and sender. This concept is called channel direction.
The way to give this access level is by adding the <-
sign before or after the chan
keyword. For more details, see the following table.
Syntax | Explanation |
---|---|
ch chan string | The ch parameter can be used to send and receive data |
ch chan<- string | The ch parameter can only be used to send data |
ch <-chan string | The ch parameter can only be used to receive data |
By default a channel will have the ability to send and receive data. In order for the channel to only be able to send or receive, we need to utilize the <-
symbol.
For example the function sendMessage(ch chan<- string)
whose parameter ch
is declared with an access level for sending data only. The channel can only be used to send data, for example: ch <- fmt.Sprintf("data %d", i)
.
And vice versa in the function printMessage(ch <-chan string)
, the channel ch
can only be used to receive data.
Channel - Timeout
Defining a channel in order to control the reception of data from the channel based on the time it was received, we need to check the timeout duration that we can determine ourselves. For example, when there is no data reception activity for a certain duration, it will trigger a callback
whose content is also self-defined.
Here is a simple program about applying timeout to channels. A new goroutine is executed with the task of sending data every certain interval, with the interval duration being random.
package main
import (
"math/rand"
"time"
)
func sendData(ch chan <- int) {
for i := 0; true; i++ {
ch <- i
time.Sleep(time.Duration(rand.Int()%10+1)) * time.Second) // certain functions take a long time to process, sometimes
}
}
Next, an endless loop is set up, on each loop there is a channel condition selection using select
.
func retreiveData(ch<-chan int) {
loop:
for {
select {
case data := <-ch:
fmt.Print(`receive data"`, data, `"`, "\n")
case < time.After(time.Second * 5):
fmt.Println("timeout. no activities under 5 seconds")
break loop
}
}
}
In the retrieveData
function there are 2 conditions which occur if
case data := <- messages:
will be fulfilled when there is a data handover on themessages
channelcase <- time.After(time.Second * 5):
then it will be fulfilled when there is no data receiving activity from the channel within 5 seconds.
Then at the end we will execute the function in the main
function. Here is more details.
func main() {
rand.Seed(time.Now().Unix())
runtime.GOMAXPROCS(2)
var messages = make(chan int)
go sendData(messages)
retreiveData(messages)
}
The output will appear every time there is data reception with a random time delay. When there is no activity on the channel within 5 seconds, the channel checking loop will be stopped.
➜ channel-timeout git:(main) ✗ go run main.go
receive data "0"
receive data "1"
timeout. no activities under 5 seconds
➜ channel-timeout git:(main) ✗ go run main.go
receive data "0"
timeout. no activities under 5 seconds
➜ channel-timeout git:(main) ✗ go run main.go
receive data "0"
timeout. no activities under 5 seconds
➜ channel-timeout git:(main) ✗ go run main.go
receive data "0"
receive data "1"
receive data "2"
timeout. no activities under 5 seconds