programming

Usage Channel Select Range Timeout

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 channel ch1 which will then be accommodated in the variable avg.
  • The condition case max := <-ch2 will be met when receiving data from channel ch2 which will then be accommodated in the variable max.

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.

SyntaxExplanation
ch chan stringThe ch parameter can be used to send and receive data
ch chan<- stringThe ch parameter can only be used to send data
ch <-chan stringThe 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 the messages channel
  • case <- 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
comments powered by Disqus