Go (Golang) is renowned for its simplicity and power in handling concurrency. At the heart of Go’s concurrency model are goroutinesโlightweight threadsโand channels for communication. Unlike traditional threading models, Go encourages sharing memory by communicating, leading to safer and more maintainable code. This post explores key concurrency patterns in Go, providing explanations and code examples for each.
Introduction to Go Concurrency
Go’s concurrency primitives make it easy to write programs that do multiple things simultaneously. Goroutines are created with the go keyword and run concurrently. Channels allow goroutines to communicate safely. Patterns build on these to solve common problems like parallel processing, data pipelines, and synchronization.
Basic Patterns
1. Goroutines and Channels
Goroutines run functions concurrently, and channels send/receive data.
Example:
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
This runs “hello” and “world” concurrently.
2. Worker Pool
Distribute tasks among a fixed number of workers.
Example:
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "processing job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 9; a++ {
<-results
}
}
Workers process jobs concurrently.
3. Pipeline
Chain operations where each stage processes and passes data.
Example:
package main
import "fmt"
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
c := gen(2, 3)
out := sq(c)
fmt.Println(<-out) // 4
fmt.Println(<-out) // 9
}
Data flows from gen to sq.
4. Fan-In/Fan-Out
Fan-out distributes work, fan-in collects results.
Example:
package main
import (
"fmt"
"sync"
)
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for val := range ch {
fmt.Printf("Consumer %d: %d\n", id, val)
}
}
func main() {
ch := make(chan int, 10)
var wg sync.WaitGroup
go producer(ch)
for i := 1; i <= 3; i++ {
wg.Add(1)
go consumer(i, ch, &wg)
}
wg.Wait()
}
Producer fans out to consumers.
5. Select for Multiplexing
Handle multiple channels with select.
Example:
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
Selects the first ready channel.
6. Context for Cancellation
Use context to cancel operations.
Example:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled\n", id)
return
default:
fmt.Printf("Worker %d working\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
Cancels workers gracefully.
7. Mutex for Shared State
Though not purely concurrency, use sync.Mutex for shared data.
Example:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func main() {
c := Counter{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
fmt.Println(c.value) // 100
}
Safely increments counter.
Best Practices
- Prefer channels over shared memory.
- Use buffered channels for performance.
- Handle panics in goroutines.
- Profile with
go tool pprof.
Conclusion
Go’s concurrency patterns simplify parallel programming. Start with basics, then apply patterns like worker pools for scalability. Experiment with these examples to build robust concurrent applications.
For more, read “Concurrency in Go” by Katherine Cox-Buday or the Go documentation.
Comments