Understanding Concurrency in Go Programming
Go's approach to concurrency is one of its most distinctive and powerful features. This guide covers everything you need to know about concurrent programming in Go.
Core Concepts
- Lightweight threads
- Concurrent execution
- Scheduling
- Best practices
- Communication
- Synchronization
- Buffered channels
- Channel patterns
- Multiple channel operations
- Timeouts
- Default cases
- Common patterns
- Synchronization
- Lock types
- Best practices
- Deadlock prevention
- Cancellation
- Timeouts
- Value propagation
- Best practices
Advanced Topics
Common Patterns
1. Basic Goroutine
// Launch concurrent operation
go func() {
// Do work
fmt.Println("Working...")
}()
2. Channel Communication
// Create channel
ch := make(chan string)
// Send data
go func() {
ch <- "Hello"
}()
// Receive data
msg := <-ch
3. Worker Pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 9; a++ {
<-results
}
}
Best Practices
1. Goroutine Management
- Always know how your goroutines will end
- Use WaitGroups for synchronization
- Handle panics in goroutines
- Consider using worker pools
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// Do work
}()
wg.Wait()
2. Channel Usage
- Use buffered channels when appropriate
- Always close channels from the sender side
- Handle closed channels gracefully
- Use directional channel parameters
// Sender
func produce(ch chan<- int) {
defer close(ch)
for i := 0; i < 5; i++ {
ch <- i
}
}
// Receiver
func consume(ch <-chan int) {
for v := range ch {
fmt.Println(v)
}
}
3. Error Handling
func doWork() error {
errCh := make(chan error, 1)
go func() {
if err := riskyOperation(); err != nil {
errCh <- err
return
}
errCh <- nil
}()
select {
case err := <-errCh:
return err
case <-time.After(time.Second):
return errors.New("timeout")
}
}
Common Concurrency Patterns
1. Pipeline Pattern
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
2. Fan-out, Fan-in
func fanOut(ch <-chan int, n int) []<-chan int {
outputs := make([]<-chan int, n)
for i := 0; i < n; i++ {
outputs[i] = sq(ch)
}
return outputs
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
merged <- n
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
Performance Considerations
Goroutine Overhead
- Goroutines are lightweight but not free
- Consider pooling for very high numbers
- Monitor goroutine count
Channel Operations
- Buffered vs unbuffered trade-offs
- Channel closure costs
- Select performance
Synchronization Costs
- Mutex vs channel trade-offs
- Lock contention
- Memory synchronization
Debugging Concurrent Programs
Race Detector
go run -race main.go go test -race ./...
Deadlock Detection
- Go runtime automatically detects deadlocks
- Use timeout patterns for prevention
Profiling
import _ "net/http/pprof"
Next Steps
- Study standard library concurrency primitives
- Learn about testing concurrent code
- Explore best practices for production code
- Practice with real-world examples