Golang Channel Patterns
Golang channel allows us to transfer data structure between go-routine boundaries. It could be zero byte data struct{}{}
, served as signaling purpose. In this use case, the channel is semantically a signaling, by which, one go-routine can send a signal to another go-routine, as a notification or an event.
Signaling without data serves the main purpose of cancellation. It allows one go-routine to signal another go-routine to cancel the channel.
Guarantee of Delivery
Does one go-routine send a signal guaranteed to be delivered to another go-routine? The guarantee delivery means that, the sender will be blocked until the receiver receives the signal. The following table shows answer.
Channel | Buffered > 1 | Unbuffered |
---|---|---|
Delivery | Not Guaranteed | Guaranteed |
The unbuffered channel is declared as follows
channel := make(chan struct{})
The buffer size is lager than one is declared as follows
channel := make(chan struct{}, 3)
Channel Patterns
Based on guarantee delivery behavior, we cloud demonstrate channel patterns.
Block and Wait Pattern
Block and wait pattern is for a sender channel<-struct{}{}
. It will block until receiver <-channel
receives the data. It guarantees the data delivery.
package main
import (
"fmt"
"sync"
)
func worker(work chan string, wg *sync.WaitGroup) {
fmt.Println("worker: waiting for work")
fmt.Println(fmt.Sprintf("worker: received the work '%s'", <-work))
wg.Done()
}
func main() {
var (
ch = make(chan string)
wg sync.WaitGroup
)
wg.Add(1)
go worker(ch, &wg)
fmt.Println("manager: send a job")
ch <- "submit a PR"
wg.Wait()
}
The running result
worker: waiting for work
manager: send a job
worker: received the work 'submit a PR'
Ticker Pattern
The time ticker is the pattern that we could schedule an event happens in certain interval. It is the same as we run a cron job.
package main
import (
"fmt"
"time"
)
func cron_job(doneChan chan struct{}) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println(fmt.Sprintf("cron job %v", time.Now()))
case <-doneChan:
fmt.Println("The cron job is done")
return
}
}
}
func main() {
var (
ch = make(chan struct{})
)
go cron_job(ch)
fmt.Println("let cron job run 3 times")
time.Sleep(3 * time.Second)
close(ch)
}
The running result
let cron job run 3 times
cron job 2020-10-27 13:39:20.621481 -0700 PDT m=+1.005642107
cron job 2020-10-27 13:39:21.618135 -0700 PDT m=+2.002336021
cron job 2020-10-27 13:39:22.620087 -0700 PDT m=+3.004329404
The cron job is done
Fan-out Pattern
Fan-out pattern will use buffered channel. As mentioned above, the sender will not block and wait for receiver, rather, the sender will keep sending data into channel until buffer is full. It will help for multiple go-routines send data to a channel and only one receiver receives the data.
The following example is assuming we have 10 go-routines but we want to run at most 5 of them at a time. We will use another channel, called semaphore channel, to control it.
package main
import (
"fmt"
"time"
)
func main() {
var (
numDevelopers = 10
capacity = 5
ch = make(chan string, numDevelopers)
sem = make(chan bool, capacity)
)
for w := 0; w < numDevelopers; w++ {
go func(developer int) {
sem <- true
{
time.Sleep(200 * time.Millisecond)
fmt.Println(fmt.Sprintf("developer %d send a PR", developer))
ch <- "PR"
}
<- sem
}(w)
}
time.Sleep(200 * time.Millisecond)
for numDevelopers > 0 {
fmt.Println(fmt.Sprintf("reviewer receives a %s", <-ch))
numDevelopers--
}
}
The running result
developer 5 send a PR
developer 1 send a PR
developer 4 send a PR
developer 3 send a PR
developer 2 send a PR
reviewer receives a PR
reviewer receives a PR
reviewer receives a PR
reviewer receives a PR
reviewer receives a PR
developer 8 send a PR
reviewer receives a PR
developer 0 send a PR
reviewer receives a PR
developer 7 send a PR
reviewer receives a PR
developer 9 send a PR
reviewer receives a PR
developer 6 send a PR
reviewer receives a PR
Drop Pattern
In the example above, we set the maximum capacity for running in one batch by using semaphore channel. If we may want to set maximum capacity and drop all others data, we could use drop pattern.
package main
import (
"fmt"
"time"
)
func main() {
var (
cap = 5
ch = make(chan string, cap)
num = 10
)
go func() {
for p := range ch {
fmt.Println("received the data from channel", p)
}
}()
for i := 0; i < num; i ++ {
select {
case ch <- "data":
fmt.Println("sent the data to the channel")
default:
fmt.Println("drop the data")
}
}
time.Sleep(1 * time.Second)
close(ch)
}
The running result
sent the data to the channel
sent the data to the channel
sent the data to the channel
sent the data to the channel
sent the data to the channel
sent the data to the channel
drop the data
drop the data
drop the data
drop the data
received the data from channel data
received the data from channel data
received the data from channel data
received the data from channel data
received the data from channel data
received the data from channel data
Conclusion
I believe channel is the most attractive feature that Golang provides. In this article, we demonstrated the following channel patterns:
- Block and Wait pattern
- Time ticker pattern
- Fan-out pattern
- Drop (max capacity) pattern