Go’s concurrency model is one of its strongest features. Here’s how to effectively use goroutines, channels, and concurrency patterns.
Goroutines
Basic Usage
// Simple goroutine
go func() {
fmt.Println("Running in goroutine")
}()
// With function
go processData(data)
WaitGroup
import "sync"
var wg sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
processItem(id)
}(i)
}
wg.Wait() // Wait for all goroutines
}
Channels
Basic Channel Operations
// Unbuffered channel
ch := make(chan int)
// Send
ch <- 42
// Receive
value := <-ch
// Close
close(ch)
Buffered Channels
// Buffered channel (capacity 10)
ch := make(chan int, 10)
// Non-blocking send if buffer not full
ch <- 1
ch <- 2
Channel Directions
// Send-only channel
func sendOnly(ch chan<- int) {
ch <- 42
}
// Receive-only channel
func receiveOnly(ch <-chan int) {
value := <-ch
}
// Bidirectional (default)
func bidirectional(ch chan int) {
ch <- 42
value := <-ch
}
Common Patterns
Worker Pool
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() {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}()
}
wg.Wait()
close(results)
}
Fan-Out, Fan-In
// Fan-out: Distribute work
func fanOut(input <-chan int, outputs []chan int) {
for value := range input {
for _, output := range outputs {
output <- value
}
}
for _, output := range outputs {
close(output)
}
}
// Fan-in: Combine results
func fanIn(inputs []<-chan int) <-chan int {
output := make(chan int)
var wg sync.WaitGroup
for _, input := range inputs {
wg.Add(1)
go func(ch <-chan int) {
defer wg.Done()
for value := range ch {
output <- value
}
}(input)
}
go func() {
wg.Wait()
close(output)
}()
return output
}
Pipeline Pattern
func pipeline(input <-chan int) <-chan int {
stage1 := make(chan int)
stage2 := make(chan int)
// Stage 1
go func() {
defer close(stage1)
for value := range input {
stage1 <- value * 2
}
}()
// Stage 2
go func() {
defer close(stage2)
for value := range stage1 {
stage2 <- value + 1
}
}()
return stage2
}
Context for Cancellation
Using Context
import "context"
func processWithContext(ctx context.Context, data []int) error {
for _, item := range data {
select {
case <-ctx.Done():
return ctx.Err()
default:
processItem(item)
}
}
return nil
}
// Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := processWithContext(ctx, data)
Context with Timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := fetchData(ctx)
Select Statement
Non-Blocking Operations
select {
case value := <-ch1:
// Handle value from ch1
case value := <-ch2:
// Handle value from ch2
case ch3 <- 42:
// Successfully sent to ch3
default:
// No channel ready
}
Timeout Pattern
select {
case result := <-ch:
return result
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
Mutex and RWMutex
Protecting Shared State
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
Read-Write Mutex
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func (m *SafeMap) Get(key string) int {
m.mu.RLock()
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key string, value int) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}
Best Practices
1. Always Use defer for Cleanup
// Good
func process() {
mu.Lock()
defer mu.Unlock()
// Process
}
// Bad
func process() {
mu.Lock()
// Process
mu.Unlock() // Might not execute if panic
}
2. Avoid Goroutine Leaks
// Good: Use context for cancellation
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
// Work
}
}
}
// Bad: Goroutine runs forever
func worker() {
for {
// Work forever
}
}
3. Close Channels Properly
// Good: Close channel when done
func producer(ch chan int) {
defer close(ch)
for i := 0; i < 10; i++ {
ch <- i
}
}
Common Pitfalls
1. Race Conditions
// Bad: Race condition
var counter int
func increment() {
counter++ // Not thread-safe
}
// Good: Use mutex
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
2. Closing Closed Channel
// Bad: Panic if channel already closed
close(ch)
close(ch) // Panic!
// Good: Check or use sync.Once
var once sync.Once
once.Do(func() { close(ch) })
Performance Tips
1. Use Buffered Channels
// For known capacity
ch := make(chan int, 100)
2. Limit Goroutine Count
// Use worker pool instead of unlimited goroutines
const maxWorkers = 10
semaphore := make(chan struct{}, maxWorkers)
3. Profile Your Code
import _ "net/http/pprof"
// Add to main
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
Conclusion
Go’s concurrency model provides:
- Goroutines: Lightweight threads
- Channels: Safe communication
- Select: Multiplexing
- Context: Cancellation and timeouts
Key principles:
- Don’t communicate by sharing memory; share memory by communicating
- Use channels for communication
- Use mutexes only when necessary
- Always handle context cancellation
- Avoid goroutine leaks
Master these patterns to write efficient, concurrent Go programs! 🚀