Concurrent programming in Go
This short course (recently refreshed for Go 1.26) aims at demystifying Go’s main concurrency features: goroutines, channels, mutexes, select statements, and contexts. Through a series of practical exercises, you’ll learn how to use concurrency to your advantage and avoid its many pitfalls.
Duration ¶
1 day (7 hours)
Prerequisites ¶
- Fluency in English
- Experience with the basics of Go but not necessarily with its concurrency features
- A basic knowledge of Git
- A personal computer (preferably running macOS or Linux) on which the following tools have been installed (ahead of the course):
- Go 1.26+
- Visual Studio Code and its Go extension, or your preferred IDE or text editor
- Git
- A stable Internet connection
Outline ¶
- Presentation of the namecheck project
- namecheck: a CLI tool for checking the validity and availability of usernames on social media (GitHub, Reddit, etc.)
- Motivations for developing such a tool
- Introduction to concurrency
- Concurrency: the art of composing a program in order to delineate independent tasks
- Concurrency vs. parallelism: close but distinct
- Concurrency enables parallelism and promises free performance gains as the number of cores increases
- Amdahl’s law and Gunther’s law: parallelism’s diminishing returns
- Go’s concurrency trifecta: goroutines, channels, et
selectstatements
- Goroutines
- Goroutines: featherweight threads
- The Go scheduler
- Starting goroutines with the
gokeyword - The golden rule: don’t start a goroutine if you can’t explain when it will terminate
- The
mainfunction doesn’t wait for other goroutines to terminate - Wait groups
- Synchronization bugs and race conditions
- Concurrency safety
- Data races
- Race detector
- Channels
- Shared-memory concurrency vs. message-passing concurrency
- Share memory by communicating
- Channels: the preferred mechanism for communicating between goroutines
- Naming conventions
- Channels are values just like any other
- The zero value of channel types
- Initializing a channel via the
makebuiltin - Capacity of a channel
- Channels are FIFO
- On the importance of choosing a channel’s capacity judiciously
- Sending a value to a channel
- Receiving a value from a channel
- Lifting ambiguity at receiption with the comma-ok idiom
- Deadlock
- Goroutine leak
- Closing a channel
- Good and bad reasons to close a channel
- Ranging over a channel
- Directional channels
- HTTP server
- Introduction to the net/http package
- A simple “Hello, World!” server
- What about Web frameworks?
- Beware: handlers are invoked concurrently
- A short detour in the land of shared-memory concurrency
- Communicating by sharing memory
- Mutexes and critical sections
- The
sync.Mutextype
- Select statements
- Multiplexing channel communications
- The
selectkeyword - Event loop: a
selectstatement nested in a loop
- Canceling goroutines
- The need to force goroutines to terminate
- Emit and detect a cancellation signal: the basic technique
- The
context.Contexttype - Emit and detect a cancellation signal: the modern approach
- Cancelable channel send
context.Background()andcontext.TODO()- Deriving contexts
- Cancellation signals propagate down the tree of contexts
- Embrace “cancel culture”: design most of your functions as cancelable
- Contexts on the client side and on the (Web) server side
- Learning resources
Labs:
- Refactor the namecheck project with concurrency in mind
- Analysis and resolution of a simple deadlock
- Analysis and resolution of a simple goroutine leak
- Aggregate the results from multiple goroutines thanks to a channel
- Turn the CLI tool into a Web server
- Configure your server for CORS thanks to a third-party library
- Use a mutex to guard a shared map storing statistics about the service in memory
- Analysis and resolution of a goroutine leak thanks to contexts
- Use contexts to cancel tasks whose results are no longer necessary