Go Concurrency Patterns
Master Go concurrency — goroutines, channels, select, WaitGroups, mutexes, and common patterns like fan-out/fan-in and worker pools.
What you'll learn
- ✓How goroutines and channels enable concurrency in Go
- ✓Select statements for multiplexing channels
- ✓Fan-out/fan-in, worker pools, and pipeline patterns
- ✓When to use channels vs mutexes
Prerequisites
- •Go basics (functions, structs, interfaces)
- •Understanding of concurrent vs parallel execution
Go was designed with concurrency as a first-class citizen. Goroutines are cheap, channels are typed, and the select statement lets you multiplex. This guide covers the patterns you will use in production.
Goroutines
A goroutine is a lightweight thread managed by the Go runtime. Start one with the go keyword.
func main() {
go sayHello("Alice")
go sayHello("Bob")
time.Sleep(time.Second) // wait for goroutines (don't do this in production)
}
func sayHello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
Goroutines cost about 2KB of stack (grows as needed) vs 1MB+ for OS threads. You can run millions of them.
Channels
Channels are typed conduits for communication between goroutines.
func main() {
ch := make(chan string)
go func() {
ch <- "hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
Buffered channels
Buffered channels allow sends without an immediate receiver, up to the buffer size.
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block — buffer full
Directional channels
Restrict channel direction in function signatures to prevent misuse.
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) {
for val := range in {
fmt.Println(val)
}
}
Select
select waits on multiple channel operations. It is the concurrency equivalent of switch.
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "two"
}()
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
}
}
Timeout with select
select {
case result := <-ch:
fmt.Println("Got result:", result)
case <-time.After(3 * time.Second):
fmt.Println("Timed out")
}
WaitGroup
sync.WaitGroup waits for a collection of goroutines to finish.
func main() {
var wg sync.WaitGroup
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
fmt.Println("Error:", err)
return
}
resp.Body.Close()
fmt.Printf("%s → %d\n", u, resp.StatusCode)
}(url)
}
wg.Wait()
fmt.Println("All requests complete")
}
Pattern: Fan-out / Fan-in
Fan-out: multiple goroutines read from the same channel. Fan-in: multiple channels feed into one.
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for val := range c {
merged <- val
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Pattern: Worker pool
A fixed number of goroutines process jobs from a shared channel.
func workerPool(jobs <-chan int, results chan<- int, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
workerPool(jobs, results, 5)
for i := 0; i < 20; i++ {
jobs <- i
}
close(jobs)
for result := range results {
fmt.Println("Result:", result)
}
}
Pattern: Pipeline
Each stage is a goroutine that receives from one channel and sends to another.
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
ch := generate(2, 3, 4, 5)
result := square(ch)
for val := range result {
fmt.Println(val) // 4, 9, 16, 25
}
}
Context for cancellation
The context package provides cancellation, deadlines, and request-scoped values.
func longRunning(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// do work
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := longRunning(ctx); err != nil {
fmt.Println("Stopped:", err) // context deadline exceeded
}
}
Channels vs mutexes
| Use case | Channels | Mutexes |
|---|---|---|
| Passing data between goroutines | Yes | No |
| Protecting shared state | Possible but awkward | Yes |
| Signaling events | Yes | No |
| Simple counters/caches | Overkill | Yes |
The proverb: “Don’t communicate by sharing memory; share memory by communicating.” But use the right tool — a sync.Mutex protecting a map is simpler than a channel-based equivalent.
Summary
Go concurrency is built on goroutines, channels, and select. The patterns — fan-out/fan-in, worker pools, pipelines — compose from these primitives. Use sync.WaitGroup for simple coordination, context for cancellation, and reach for mutexes when protecting shared state is simpler than channel choreography.
Related articles
- Go Go Concurrency: Fan-Out, Pipeline, and Worker Pool
Master advanced Go concurrency patterns including fan-out/fan-in, pipelines, and worker pools with practical examples and production-ready code.
- Go Go Channel and Select Statement Patterns
Master Go channels and select statements with patterns for fan-out, fan-in, timeouts, cancellation, and pipeline composition.
- Go Real-World Go Generics Examples Beyond Tutorials
Practical Go generics patterns for production code: type-safe collections, result types, repository layers, and functional helpers.
- Go Race Detector and Debugging Concurrent Go Code
Use Go's race detector to find and fix data races, plus tools and patterns for debugging concurrent programs effectively.