Yesterday, I published an article that visually explained how channels in Golang work. If you still have difficulty understanding channels, it's best to check out that article before reading this article. As a quick refresher: Partier, Candier, and Stringer run a coffee shop. Partier assists in receiving orders from customers and then passes those orders to the kitchen, where Candier and Stringer make the coffee.
Gophers Cafe
In this article, I will visually explain select
statement, another powerful tool for handling concurrency in Go applications. Gophers and their imaginary cafes will still be my companions, but this time, let's focus on the Partier and ordering parts.
Scenes
Gophers Cafe is aware that more and more customers want to order coffee through food delivery apps. So, in addition to ordering on-site, they also opted for a food delivery app. queue
Partier monitors orders from these two channels at the same time and forwards these orders to Candier and Stringer through another channel .
select {
case order := <-appOrders:
queue <- order
case order := <-inShopOrders:
queue <- order
}
When any channel receives an order, the Partier forwards it to queue
the channel.
If there are orders on both channels, one of them will be selected. In a real cafe, inShopOrders
orders from . However, in a Go application we cannot guarantee which order will be selected. Also note that select
each execution of the statement will only select one order, Partier will not select one order and then another. Nonetheless, in many applications select
the statements are usually for
inside a loop so that orders left in the previous iteration have a chance of being picked in the next iteration.
for {
select {
case order := <-appOrders:
queue <- order
case order := <-inShopOrders:
queue <- order
}
}
However, if both channels have orders, they will compete fairly again.
Default branch
During off-peak hours, there are not many orders and Partier spends a lot of time waiting. He figured he could use his time more productively by doing other things, such as cleaning his desk. This can be default
achieved by.
for {
select {
case order := <-appOrders:
log.Println("There is an order coming from appOrders channel")
queue <- order
case order := <-inShopOrders:
log.Println("There is an order coming from inShopOrders channel")
queue <- order
default:
log.Println("There is no order on both channels, I will do cleaning instead")
doCleaning()
}
}
time.After()
Typically used time.After(duration)
with select
to prevent waiting forever. Instead of default
executing immediately when no channel is available, time.After(duration)
create a normal one <-chan Time
, wait for duration
it to elapse, and then send the current time on the returned channel. This channel select
is treated like other channels in the statement. As you can see, select
the channels in the statement can be of different types.
shouldClose := false
closeHourCh := time.After(8 * time.Hour)
for !shouldClose {
select {
case order := <-appOrders:
log.Println("There is an order coming from appOrders channel")
queue <- order
case order := <-inShopOrders:
log.Println("There is an order coming from inShopOrders channel")
queue <- order
case now := <-closeHourCh:
log.Printf("It is %v now, the shop is closing\n", now)
shouldClose = true
default:
log.Println("There is no order on both channels, I will go cleaning instead")
doCleaning()
}
}
log.Println("Shop is closed, I'm going home now. Bye!")
This technique is very common when dealing with remote API calls because we cannot guarantee when or if the remote server will return. With that context
, we usually don't need to do this.
responseChannel := make(chan interface{})
timer := time.NewTimer(timeout)
select {
case resp := <-responseChannel:
log.Println("Processing response")
processResponse(resp)
timer.Stop()
case <-timer.C:
log.Println("Time out, giving up")
}
Sample code
Let's end this article with a complete fictional cafe code. One more thing to note here, selecting from a closed channel will always return immediately. So if you think it's necessary, use the "comma ok" idiom. Hands-on coding is the best way to learn programming. So if you're select
not too familiar with it, I recommend copying it on your IDE and trying to modify this code.
Happy coding!
package main
import (
"fmt"
"log"
"time"
)
func main() {
appOrders := make(chan order, 3)
inShopOrders := make(chan order, 3)
queue := make(chan order, 3)
go func() {
for i := 0; i < 6; i++ {
appOrders <- order(100 + i)
time.Sleep(10 * time.Second)
}
close(appOrders)
}()
go func() {
for i := 0; i < 4; i++ {
inShopOrders <- order(200 + i)
time.Sleep(15 * time.Second)
}
close(inShopOrders)
}()
go partier(appOrders, inShopOrders, queue)
for o := range queue {
log.Printf("Served %s\n", o)
}
log.Println("Done!")
}
func partier(appOrders <-chan order, inShopOrders <-chan order, queue chan<- order) {
shouldClose := false
closeTimeCh := time.After(1 * time.Minute)
for !shouldClose {
select {
case ord, ok := <-appOrders:
if ok {
log.Printf("There is %s coming from appOrders channel\n", ord)
queue <- ord
}
case ord, ok := <-inShopOrders:
if ok {
log.Printf("There is %s coming from inShopOrders channel\n", ord)
queue <- ord
}
case now := <-closeTimeCh:
log.Printf("It is %v now, the shop is closing\n", now)
shouldClose = true
default:
log.Println("There is no order on both channels, I will go cleaning instead")
doCleaning()
}
}
close(queue)
log.Println("Shop is closed, I'm going home now. Bye!")
}
func doCleaning() {
time.Sleep(5 * time.Second)
log.Println("Partier: Cleaning done")
}
type order int
func (o order) String() string {
return fmt.Sprintf("order-%02d", o)
}
Thank you for reading until the end of the article. Please consider following the author~