Understanding Concurrency Patterns in Go

Comparing sync and channel approach in goroutines

Go is an awesome language with great built-in support for concurrency. However, I feel that this topic isn’t very much in the clear, often discussed lightly at the end of the books, and keeping many away from using Go in production.

sync.WaitGroup (low-level style)

sync is a package providing basic synchronization primitives for goroutines. sync.WaitGroup is very popular, since it’s slightly faster than channels and is easy to understand.

var wg sync.WaitGroup
func main() {
words := []string{ "foo", "bar", "baz" }

for _, word := range words {
                // add one backlog to the group
wg.Add(1)
                go func(word string) {
                        // Block for a second
time.Sleep(1 * time.Second)
                        // remove that backlog from the group
defer wg.Done()
                        fmt.Println(word)
}(word)
}

// Keep on doing things on the main goroutine
fmt.Println(1)
time.Sleep(1 * time.Second)
fmt.Println(2)
time.Sleep(1 * time.Second)
fmt.Println(3)
        // This waits (block) for all goroutines to finish.
wg.Wait()
}

Run in the playground.

Channels

Channels are a unique primitive in Go, serving as a fruition of the great doctrine from Go’s authors:

Don’t communicate by sharing memory; share memory by communicating.

If you have programmed anything remotely similar to a queue (i.e. a Python queue), channels are not so different.

Here is the equivalent of the above snippet, rewritten using a channel.

func main() {
        words = []string{ "foo", "bar", "baz" }
        // Create a channel for communication
done := make(chan bool)
        // It is nice to close the channels, like files,
// after it's used or you may get leaks
defer close(done)
        for _, word := range words {
go func(url string) {
                        // block for a sec
time.Sleep(1 * time.Second)
                        fmt.Println(word)

// send signal to the channel
done <- true
}(word)
}
        // Do what you have to do
fmt.Println(1)
time.Sleep(1 * time.Second)
fmt.Println(2)
time.Sleep(1 * time.Second)
fmt.Println(3)
        // This blocks and waits 
<-done
}

Run in the playground.

This is straightforward and produces similar outcome to the sync.WaitGroup approach. The advantages of using channels are

  • you don’t have to manage the waitgroup with Add() and Done(), removing one more error-prone step to deadlocking.
  • Channel is a better abstraction of synchronizing communication in general (akin event in Javascript) which makes it easy to relate to real-world communications.

Request-Response Mock in Channels

To see how using channels are simpler, check out a simple mock of client-server communication with a channel below

package main
import (
“fmt”
“time”
)
type Client struct { Request string }
type Server struct { Response string }
func main() {
conn:= make(chan string, 1)
done:= make(chan bool)
defer close(conn)
defer close(done)
        client := &Client{ “ping” }
server := &Server{ “ack” }
        // Client sends request
conn <- client.Request
fmt.Printf(“CLIENT sent %s\n”, client.Request)
        go func(c chan string) {
                // receive from conn
request := <-conn
                fmt.Printf(“SERVER received: %s\n”, request)
fmt.Println(“SERVER sending response…”)
                // simulate server delay             
<-time.Tick(2 * time.Second)
                // send to conn
conn <- server.Response
fmt.Printf(“SERVER sent: %s\n”, server.Response)
// send a signal to unblock the main loop
done <- true
}(conn)
        fmt.Println(“CLIENT waiting…”)
// block until value is received from done
<-done
// receive from conn
resp := <-conn
fmt.Printf(“CLIENT received %s\n”, resp)
}

Run in the playground.

This is like how you should be able to send a request and receive a response from a single connection.

Take a look at Code Walk: Share Memory by Communicating if you’re into a few minutes of sitting and reading code.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.