Go Channel Patterns

Posted by Henry Du on Tuesday, October 27, 2020

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