Go Concurrency Patterns
By Vitalii Perehonchuk, Software Developer
www.eliftech.com
Let’s lets get through...
1. Projects not using concurrency
2. Atomic operations
3. Timeouts and tickers
4. Exit control
5. Event handling
6. Error handling
7. Tests and benchmarks
8. Something really interesting
www.eliftech.com
No concurrency in
▪ GoCV (computer vision)
▪ upper/db (drivers for MySQL,
PostgreSQL, SQLite, MongoDB,
etc)
▪ goquery (jQuery port)
▪ authboss (Auth library)
▪ go-jose (JS-way data signing and
encryption library)
▪ go-oauth2-server
▪ goth (Multi-provider auth library)
▪ oauth2 (OAuth2 library)
▪ docopt-go (Toolset for pretty CLI)
▪ go-flags (CLI options parser)
▪ kingpin (CLI library)
▪ urfave/cli (CLI library)
▪ asciigraph (lightweight ASCII line
graphs in CLI)
▪ go-prompt (CLI library)
▪ godotenv (.env library)
▪ ini (.ini library)
▪ gods (Go data structures)
▪ ...
www.eliftech.com
Atomic operations
www.eliftech.com
Cockroach - scalable & transactional datastore
Uuid generator
// Returns the epoch and clock sequence.
func (g *Gen) getClockSequence() (uint64, uint16, error) {
g.clockSequenceOnce.Do(func() {
buf := make([]byte, 2)
io.ReadFull(g.rand, buf)
g.clockSequence = binary.BigEndian.Uint16(buf)
})
...
www.eliftech.com
goleveldb - fast key-value storage library
type memdbReleaser struct {
once sync.Once
m *memDB
}
func (mr *memdbReleaser) Release() {
mr.once.Do(func() {
mr.m.decref()
})
}
www.eliftech.com
Groupcache - cache library
// An AtomicInt is an int64 to be accessed atomically.
type AtomicInt int64
func (i *AtomicInt) Add(n int64) {
atomic.AddInt64((*int64)(i), n)
}
func (i *AtomicInt) Get() int64 {
return atomic.LoadInt64((*int64)(i))
}
func (i *AtomicInt) String() string {
return strconv.FormatInt(i.Get(), 10)
}
www.eliftech.com
Tickers and timeouts
www.eliftech.com
tebeka/selenium - WebDriver client for Go
go func() {
bufr := bufio.NewReader(r)
s, err :=
bufr.ReadString('n')
ch <- resp{s, err}
}()
var display string
select {
case resp := <-ch:
if resp.err != nil {
return nil,
resp.err
}
display =
strings.TrimSpace(resp.display)
case <-time.After(3 * time.Second):
return nil,
errors.New("timeout waiting for
Xvfb")
}
www.eliftech.com
minimp3 - lightweight MP3 decoder library
// Started check the record mp3 stream started or not.
func (dec *Decoder) Started() (channel chan bool) {
channel = make(chan bool)
go func() {
for {
select {
case <-dec.context.Done():
channel <- false
default:
}
...
www.eliftech.com
minimp3 - lightweight MP3 decoder library
...
if len(dec.decodedData) != 0 {
channel <- true
} else {
<-time.After(time.Millisecond *
100)
}
}
}()
return channel
}
www.eliftech.com
casbin - auth library with access control models support
func (e *SyncedEnforcer) StartAutoLoadPolicy(d time.Duration) {
e.autoLoad = true
go func() {
n := 1
for {
if !e.autoLoad { break }
e.LoadPolicy()
n++
time.Sleep(d)
}
}()
}
www.eliftech.com
minimp3 - lightweight MP3 decoder library
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
if atomic.LoadInt32(&stopAll) > 0 {
return
}
if time.Now().After(endTs) {
return
}
}
www.eliftech.com
rqlite - lightweight relational database with SQLite storage
// checkConnections periodically checks which connections should
close due to timeouts.
func (s *Store) checkConnections() {
s.wg.Add(1)
ticker := time.NewTicker(s.connPollPeriod)
go func() {
defer s.wg.Done()
defer ticker.Stop()
for {
select {
case <-s.done: return
case <-ticker.C:
...
www.eliftech.com
BigCache - efficient key-value store for gigabytes
ticker := time.NewTicker(config.CleanWindow)
defer ticker.Stop()
for {
select {
case t := <-ticker.C:
cache.cleanUp(uint64(t.Unix()))
case <-cache.close:
return
}
}
www.eliftech.com
Exit control
www.eliftech.com
telegram-bot-api - Telegram bot client
// GetUpdatesChan starts and returns a channel for getting updates.
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig)
(UpdatesChannel, error) {
ch := make(chan Update, bot.Buffer)
go func() {
for {
select {
case <-bot.shutdownChannel: return
default:
}
updates, err := bot.GetUpdates(config)
...
www.eliftech.com
telegram-bot-api - Telegram bot client
...
if err != nil {
time.Sleep(time.Second * 3)
continue
}
for _, update := range updates {
if update.UpdateID >= config.Offset
{
config.Offset =
update.UpdateID + 1
ch <- update
}
}}}()
return ch, nil
}
www.eliftech.com
Drone - CI platform built on Docker
import "golang.org/x/sync/errgroup"
var g errgroup.Group
g.Go(func() error {
return s1.ListenAndServe()
})
g.Go(func() error {
return
s2.ListenAndServeTLS(
s.Cert,
s.Key,
)
})
g.Go(func() error {
select {
case <-ctx.Done():
s1.Shutdown(ctx)
s2.Shutdown(ctx)
return nil
}
})
g.Wait()
www.eliftech.com
Event handling
www.eliftech.com
rakyll/portmidi - Go bindings for PortMidi
// Listen input stream for MIDI events.
func (s *Stream) Listen() <-chan Event {
ch := make(chan Event)
go func(s *Stream, ch chan Event) {
for {
// sleep for a while before the new polling
tick,
// otherwise operation is too intensive and
blocking
time.Sleep(10 * time.Millisecond)
events, err := s.Read(1024)
// Note: It's not very reasonable to push sliced data
into
// a channel, several perf penalties there
are.
// This function is added as a handy utility.
if err != nil { continue }
for i := range events { ch <- events[i] }
}
}(s, ch)
return ch
www.eliftech.com
gocui - minimalist library for CLI
go func() {
g.tbEvents <- termbox.Event{
Type: termbox.EventResize,
}
}()
for {
select {
case ev := <-g.tbEvents:
g.handleEvent(&ev)
case ev := <-g.userEvents:
ev.f(g)
}
g.consumeevents()
g.flush()
}
www.eliftech.com
Error handling
www.eliftech.com
dgraph - graph database
...
che := make(chan error, 1)
chb := make(chan []byte, 1000)
go func() {
che <- writeToFile(output, chb)
}()
…
return che
www.eliftech.com
Tests and benchmarks
www.eliftech.com
`go test` command runs tests in goroutines,
thus - concurrently, but not parallelly by default.
Having run `t.Parallel()` within its code, a test is run parallelly with another parallel tests (but
never - with other runs of itself).
www.eliftech.com
Many libraries use goroutines only in tests and examples - to test itselves against race
conditions.
www.eliftech.com
goleveldb - fast key-value storage library
b.SetParallelism(10)
b.RunParallel(func(pb *testing.PB) {
…
})
www.eliftech.com
TiDB - distributed HTAP database
// batchRW makes sure conflict free.
func batchRW(value []byte) {
wg := sync.WaitGroup{}
wg.Add(*workerCnt)
for i := 0; i < *workerCnt; i++ {
go func(i int) {
defer wg.Done()
…
}(i)
}
wg.Wait()
}
www.eliftech.com
1 million requests per
minute?..
www.eliftech.com
Malwarebytes
type Dispatcher struct {
// A pool of workers channels that are registered with the
dispatcher
WorkerPool chan chan Job
}
func NewDispatcher(maxWorkers int) *Dispatcher {
pool := make(chan chan Job, maxWorkers)
return &Dispatcher{WorkerPool: pool}
}
...
www.eliftech.com
Malwarebytes
...
func (d *Dispatcher) Run() {
// starting n number of workers
for i := 0; i < d.maxWorkers; i++ {
worker := NewWorker(d.pool)
worker.Start()
}
go d.dispatch()
}
...
www.eliftech.com
Malwarebytes
...
func (d *Dispatcher) dispatch() {
for {
select {
case job := <-JobQueue:
// a job request has been received
go func(job Job) {
// try to obtain a worker job channel
that is available.
// this will block until a worker is
idle
jobChannel := <-d.WorkerPool
// dispatch the job to the worker job
channel
jobChannel <- job
}(job)
}
}
}
www.eliftech.com
Malwarebytes
// Start method starts the run loop for the worker, listening for a quit
channel in
// case we need to stop it
func (w Worker) Start() {
go func() {
for {
// register the current worker into the worker
queue.
w.WorkerPool <- w.JobChannel
select {
case job := <-w.JobChannel:
// we have received a work request.
job.Payload.UploadToS3()
case <-w.quit:
return // we have received a signal to
stop
}
}
}()
}
www.eliftech.com
Conclusion
Concurrency and parallelism are widely used by Open
Source projects in Go.
Concurrency and parallelism are NOT used by many Open
Source projects in Go.
Parallel testing and benchmarking is a common practice.
www.eliftech.com
Sources
1. github.com
2. gopkg.in
3. godoc.org
4. golang.org
5. “Handling 1 Million Requests per Minute with Go“ by Marcio Castilho
www.eliftech.com
Don't forget to subscribe not to
miss our next presentations!
Find us at eliftech.com
Have a question? Contact us:
info@eliftech.com

Go Concurrency Patterns

  • 1.
    Go Concurrency Patterns ByVitalii Perehonchuk, Software Developer
  • 2.
    www.eliftech.com Let’s lets getthrough... 1. Projects not using concurrency 2. Atomic operations 3. Timeouts and tickers 4. Exit control 5. Event handling 6. Error handling 7. Tests and benchmarks 8. Something really interesting
  • 3.
    www.eliftech.com No concurrency in ▪GoCV (computer vision) ▪ upper/db (drivers for MySQL, PostgreSQL, SQLite, MongoDB, etc) ▪ goquery (jQuery port) ▪ authboss (Auth library) ▪ go-jose (JS-way data signing and encryption library) ▪ go-oauth2-server ▪ goth (Multi-provider auth library) ▪ oauth2 (OAuth2 library) ▪ docopt-go (Toolset for pretty CLI) ▪ go-flags (CLI options parser) ▪ kingpin (CLI library) ▪ urfave/cli (CLI library) ▪ asciigraph (lightweight ASCII line graphs in CLI) ▪ go-prompt (CLI library) ▪ godotenv (.env library) ▪ ini (.ini library) ▪ gods (Go data structures) ▪ ...
  • 4.
  • 5.
    www.eliftech.com Cockroach - scalable& transactional datastore Uuid generator // Returns the epoch and clock sequence. func (g *Gen) getClockSequence() (uint64, uint16, error) { g.clockSequenceOnce.Do(func() { buf := make([]byte, 2) io.ReadFull(g.rand, buf) g.clockSequence = binary.BigEndian.Uint16(buf) }) ...
  • 6.
    www.eliftech.com goleveldb - fastkey-value storage library type memdbReleaser struct { once sync.Once m *memDB } func (mr *memdbReleaser) Release() { mr.once.Do(func() { mr.m.decref() }) }
  • 7.
    www.eliftech.com Groupcache - cachelibrary // An AtomicInt is an int64 to be accessed atomically. type AtomicInt int64 func (i *AtomicInt) Add(n int64) { atomic.AddInt64((*int64)(i), n) } func (i *AtomicInt) Get() int64 { return atomic.LoadInt64((*int64)(i)) } func (i *AtomicInt) String() string { return strconv.FormatInt(i.Get(), 10) }
  • 8.
  • 9.
    www.eliftech.com tebeka/selenium - WebDriverclient for Go go func() { bufr := bufio.NewReader(r) s, err := bufr.ReadString('n') ch <- resp{s, err} }() var display string select { case resp := <-ch: if resp.err != nil { return nil, resp.err } display = strings.TrimSpace(resp.display) case <-time.After(3 * time.Second): return nil, errors.New("timeout waiting for Xvfb") }
  • 10.
    www.eliftech.com minimp3 - lightweightMP3 decoder library // Started check the record mp3 stream started or not. func (dec *Decoder) Started() (channel chan bool) { channel = make(chan bool) go func() { for { select { case <-dec.context.Done(): channel <- false default: } ...
  • 11.
    www.eliftech.com minimp3 - lightweightMP3 decoder library ... if len(dec.decodedData) != 0 { channel <- true } else { <-time.After(time.Millisecond * 100) } } }() return channel }
  • 12.
    www.eliftech.com casbin - authlibrary with access control models support func (e *SyncedEnforcer) StartAutoLoadPolicy(d time.Duration) { e.autoLoad = true go func() { n := 1 for { if !e.autoLoad { break } e.LoadPolicy() n++ time.Sleep(d) } }() }
  • 13.
    www.eliftech.com minimp3 - lightweightMP3 decoder library ticker := time.NewTicker(time.Second) defer ticker.Stop() for range ticker.C { if atomic.LoadInt32(&stopAll) > 0 { return } if time.Now().After(endTs) { return } }
  • 14.
    www.eliftech.com rqlite - lightweightrelational database with SQLite storage // checkConnections periodically checks which connections should close due to timeouts. func (s *Store) checkConnections() { s.wg.Add(1) ticker := time.NewTicker(s.connPollPeriod) go func() { defer s.wg.Done() defer ticker.Stop() for { select { case <-s.done: return case <-ticker.C: ...
  • 15.
    www.eliftech.com BigCache - efficientkey-value store for gigabytes ticker := time.NewTicker(config.CleanWindow) defer ticker.Stop() for { select { case t := <-ticker.C: cache.cleanUp(uint64(t.Unix())) case <-cache.close: return } }
  • 16.
  • 17.
    www.eliftech.com telegram-bot-api - Telegrambot client // GetUpdatesChan starts and returns a channel for getting updates. func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) { ch := make(chan Update, bot.Buffer) go func() { for { select { case <-bot.shutdownChannel: return default: } updates, err := bot.GetUpdates(config) ...
  • 18.
    www.eliftech.com telegram-bot-api - Telegrambot client ... if err != nil { time.Sleep(time.Second * 3) continue } for _, update := range updates { if update.UpdateID >= config.Offset { config.Offset = update.UpdateID + 1 ch <- update } }}}() return ch, nil }
  • 19.
    www.eliftech.com Drone - CIplatform built on Docker import "golang.org/x/sync/errgroup" var g errgroup.Group g.Go(func() error { return s1.ListenAndServe() }) g.Go(func() error { return s2.ListenAndServeTLS( s.Cert, s.Key, ) }) g.Go(func() error { select { case <-ctx.Done(): s1.Shutdown(ctx) s2.Shutdown(ctx) return nil } }) g.Wait()
  • 20.
  • 21.
    www.eliftech.com rakyll/portmidi - Gobindings for PortMidi // Listen input stream for MIDI events. func (s *Stream) Listen() <-chan Event { ch := make(chan Event) go func(s *Stream, ch chan Event) { for { // sleep for a while before the new polling tick, // otherwise operation is too intensive and blocking time.Sleep(10 * time.Millisecond) events, err := s.Read(1024) // Note: It's not very reasonable to push sliced data into // a channel, several perf penalties there are. // This function is added as a handy utility. if err != nil { continue } for i := range events { ch <- events[i] } } }(s, ch) return ch
  • 22.
    www.eliftech.com gocui - minimalistlibrary for CLI go func() { g.tbEvents <- termbox.Event{ Type: termbox.EventResize, } }() for { select { case ev := <-g.tbEvents: g.handleEvent(&ev) case ev := <-g.userEvents: ev.f(g) } g.consumeevents() g.flush() }
  • 23.
  • 24.
    www.eliftech.com dgraph - graphdatabase ... che := make(chan error, 1) chb := make(chan []byte, 1000) go func() { che <- writeToFile(output, chb) }() … return che
  • 25.
  • 26.
    www.eliftech.com `go test` commandruns tests in goroutines, thus - concurrently, but not parallelly by default. Having run `t.Parallel()` within its code, a test is run parallelly with another parallel tests (but never - with other runs of itself).
  • 27.
    www.eliftech.com Many libraries usegoroutines only in tests and examples - to test itselves against race conditions.
  • 28.
    www.eliftech.com goleveldb - fastkey-value storage library b.SetParallelism(10) b.RunParallel(func(pb *testing.PB) { … })
  • 29.
    www.eliftech.com TiDB - distributedHTAP database // batchRW makes sure conflict free. func batchRW(value []byte) { wg := sync.WaitGroup{} wg.Add(*workerCnt) for i := 0; i < *workerCnt; i++ { go func(i int) { defer wg.Done() … }(i) } wg.Wait() }
  • 30.
  • 31.
    www.eliftech.com Malwarebytes type Dispatcher struct{ // A pool of workers channels that are registered with the dispatcher WorkerPool chan chan Job } func NewDispatcher(maxWorkers int) *Dispatcher { pool := make(chan chan Job, maxWorkers) return &Dispatcher{WorkerPool: pool} } ...
  • 32.
    www.eliftech.com Malwarebytes ... func (d *Dispatcher)Run() { // starting n number of workers for i := 0; i < d.maxWorkers; i++ { worker := NewWorker(d.pool) worker.Start() } go d.dispatch() } ...
  • 33.
    www.eliftech.com Malwarebytes ... func (d *Dispatcher)dispatch() { for { select { case job := <-JobQueue: // a job request has been received go func(job Job) { // try to obtain a worker job channel that is available. // this will block until a worker is idle jobChannel := <-d.WorkerPool // dispatch the job to the worker job channel jobChannel <- job }(job) } } }
  • 34.
    www.eliftech.com Malwarebytes // Start methodstarts the run loop for the worker, listening for a quit channel in // case we need to stop it func (w Worker) Start() { go func() { for { // register the current worker into the worker queue. w.WorkerPool <- w.JobChannel select { case job := <-w.JobChannel: // we have received a work request. job.Payload.UploadToS3() case <-w.quit: return // we have received a signal to stop } } }() }
  • 35.
    www.eliftech.com Conclusion Concurrency and parallelismare widely used by Open Source projects in Go. Concurrency and parallelism are NOT used by many Open Source projects in Go. Parallel testing and benchmarking is a common practice.
  • 36.
    www.eliftech.com Sources 1. github.com 2. gopkg.in 3.godoc.org 4. golang.org 5. “Handling 1 Million Requests per Minute with Go“ by Marcio Castilho
  • 37.
    www.eliftech.com Don't forget tosubscribe not to miss our next presentations! Find us at eliftech.com Have a question? Contact us: info@eliftech.com