Common Go Concurrency Errors and Best Practices
This article examines frequent mistakes in Go's concurrent programming—such as confusing concurrency with parallelism, assuming concurrency always speeds up execution, misusing channels versus mutexes, overlooking workload types, and misunderstanding contexts—provides detailed explanations, potential impacts, and best‑practice solutions with improved code examples.
Concurrent programming is a major strength of the Go language, thanks to goroutine and channel features. However, developers often encounter common errors such as goroutine leaks, race conditions, and improper channel usage, which can cause logical bugs or performance bottlenecks.
Error 55: Confusing Concurrency and Parallelism (#55)
Many developers mix up concurrency (the ability to handle multiple tasks) with parallelism (executing tasks simultaneously on multiple cores). Understanding the distinction helps design more efficient programs.
Possible impact: Treating concurrency as always parallel can lead to unnecessary goroutines on single‑core systems, increasing context‑switch overhead and reducing performance.
Best practice: Use concurrency for structuring programs and parallelism for CPU‑bound work; benchmark to choose the right model.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1) // limit to single core
// concurrency example
go func() {
for i := 0; i < 5; i++ {
fmt.Println("FunTester 并发 goroutine:", i)
time.Sleep(100 * time.Millisecond)
}
}()
// parallel example
for i := 0; i < 5; i++ {
fmt.Println("FunTester 主 goroutine:", i)
time.Sleep(100 * time.Millisecond)
}
time.Sleep(1 * time.Second)
}Error 56: Assuming Concurrency Is Always Faster (#56)
Concurrency does not guarantee speed improvements, especially for lightweight tasks or when system resources are limited.
Possible impact: Excessive goroutine creation can increase scheduling overhead and degrade performance.
Best practice: Benchmark different approaches; limit goroutine count to match CPU cores for CPU‑bound work and scale appropriately for I/O‑bound work.
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func compute(i int) {
time.Sleep(100 * time.Millisecond)
fmt.Println("FunTester: 计算任务", i)
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) { defer wg.Done(); compute(i) }(i)
}
wg.Wait()
fmt.Printf("FunTester: 并发执行耗时 %v\n", time.Since(start))
start = time.Now()
for i := 0; i < 5; i++ { compute(i) }
fmt.Printf("FunTester: 串行执行耗时 %v\n", time.Since(start))
}Error 57: Not Knowing When to Use Channels or Mutexes (#57)
Channels are ideal for communication between goroutines, while mutexes protect shared mutable state. Mixing them leads to unnecessary complexity or performance issues.
Best practice: Use mutexes (sync.Mutex or sync.RWMutex) for protecting shared variables; use channels for passing data or signals between goroutines.
package main
import (
"fmt"
"sync"
)
type FunTester struct { Name string; Age int }
func main() {
// channels example (synchronization only)
done := make(chan bool)
testers := []FunTester{{"FunTester1",25},{"FunTester2",30}}
for i := range testers {
go func(t *FunTester) { t.Age++; done <- true }(&testers[i])
}
for range testers { <-done }
fmt.Println("Modified testers:", testers)
// mutexes example (safe shared access)
var mu sync.Mutex
testersMutex := []FunTester{{"FunTester1",25},{"FunTester2",30}}
var wg sync.WaitGroup
wg.Add(len(testersMutex))
for i := range testersMutex {
go func(i int) { defer wg.Done(); mu.Lock(); testersMutex[i].Age++; mu.Unlock() }(i)
}
wg.Wait()
fmt.Println("Mutex modified testers:", testersMutex)
}Error 58: Not Understanding Race Conditions and Go Memory Model (#58)
Data races occur when multiple goroutines access the same memory without proper synchronization, while race conditions refer to nondeterministic execution order affecting program logic.
Best practice: Protect shared data with sync.Mutex, sync.RWMutex, or channels; use go run -race to detect races.
package main
import (
"fmt"
"sync"
)
type FunTester struct { Name string; Age int }
func main() {
var wg sync.WaitGroup
wg.Add(2)
var ft FunTester
var mu sync.Mutex
go func() { defer wg.Done(); mu.Lock(); ft.Name = "FunTesterA"; mu.Unlock() }()
go func() { defer wg.Done(); mu.Lock(); ft.Age = 30; mu.Unlock() }()
wg.Wait()
fmt.Printf("FunTester: %+v\n", ft)
}Error 59: Ignoring Workload Types for Concurrency (#59)
CPU‑intensive and I/O‑intensive tasks require different concurrency strategies. Over‑provisioning goroutines for CPU‑bound work causes excessive context switches, while I/O‑bound work benefits from many goroutines.
Best practice: Match goroutine count to CPU cores for CPU‑bound tasks; increase goroutine count for I/O‑bound tasks; benchmark to find optimal settings.
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func cpuIntensiveTask(id int) {
sum := 0
for i := 0; i < 1e7; i++ { sum += i }
fmt.Printf("CPU task %d done, sum=%d\n", id, sum)
}
func ioIntensiveTask(id int) {
fmt.Printf("IO task %d start\n", id)
time.Sleep(500 * time.Millisecond)
fmt.Printf("IO task %d done\n", id)
}
func main() {
cpuCores := runtime.NumCPU()
runtime.GOMAXPROCS(cpuCores)
var wg sync.WaitGroup
fmt.Println("Start CPU tasks")
for i := 0; i < cpuCores; i++ { wg.Add(1); go func(id int){ defer wg.Done(); cpuIntensiveTask(id) }(i) }
wg.Wait()
fmt.Println("All CPU tasks finished")
fmt.Println("Start IO tasks")
for i := 0; i < 100; i++ { wg.Add(1); go func(id int){ defer wg.Done(); ioIntensiveTask(id) }(i) }
wg.Wait()
fmt.Println("All IO tasks finished")
}Error 60: Misunderstanding Go Contexts (#60)
Contexts carry cancellation signals, deadlines, and values across API boundaries. Improper use—such as cancelling too early, sharing a root context, or ignoring cancellation—leads to resource leaks and zombie goroutines.
Best practice: Pass context as the first argument to functions, cancel when work is done, and avoid creating new root contexts inside libraries.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("goroutine %d received cancel, exiting\n", id)
return
default:
fmt.Printf("goroutine %d working\n", id)
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ { go worker(ctx, i) }
time.Sleep(1 * time.Second)
fmt.Println("Canceling context")
cancel()
time.Sleep(500 * time.Millisecond)
fmt.Println("Program finished")
}FunTester
10k followers, 1k articles | completely useless
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.