7 Common Go Concurrency Pitfalls and How to Avoid Them
This article examines frequent mistakes developers make when writing concurrent Go programs—such as misusing context, leaking goroutines, mishandling channels, and causing data races—and provides concrete code examples, impact analyses, and best‑practice recommendations to write safer, more efficient Go concurrency code.
1. Passing an Inappropriate Context
Incorrect example:
package main
import (
"context"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// Directly passing the request context without knowing when it will be cancelled
doSomething(r.Context())
}
func doSomething(ctx context.Context) {
// Continuing operation after the context is cancelled
select {
case <-ctx.Done():
// Incorrectly ignoring the cancellation signal
}
}Impact analysis: If the context cancellation is not handled properly, resources may leak or goroutines may block, especially after an HTTP request finishes.
Best practice: Always monitor the ctx.Done() signal and exit gracefully when it is triggered.
func doSomething(ctx context.Context) {
select {
case <-ctx.Done():
// Stop operation promptly
return
default:
// Continue processing
}
}2. Starting a Goroutine Without Knowing When It Stops
Incorrect example:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
for {
fmt.Println("FunTester Goroutine")
time.Sleep(1 * time.Second) // Infinite loop, goroutine never stops
}
}()
time.Sleep(3 * time.Second)
}Impact analysis: Goroutine leaks consume memory and CPU, potentially degrading system performance or causing crashes.
Best practice: Use context.Context or a dedicated channel to signal and control goroutine termination.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Goroutine exiting")
return
default:
fmt.Println("FunTester Goroutine")
time.Sleep(1 * time.Second)
}
}
}(ctx)
time.Sleep(4 * time.Second)
}3. Misusing Goroutine Loop Variables
Incorrect example:
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // i is captured by reference, may print unexpected values
}()
}
time.Sleep(1 * time.Second)
}Impact analysis: Goroutines capture the address of the loop variable, leading to unpredictable output.
Best practice: Create a local copy of the variable or pass it as a parameter to the goroutine.
func main() {
for i := 0; i < 5; i++ {
i := i // create a local copy
go func() {
fmt.Println(i) // correctly prints 0 to 4
}()
}
time.Sleep(1 * time.Second)
}4. Assuming Deterministic Order in select with Multiple Ready Channels
Incorrect example:
package main
import (
"fmt"
)
func main() {
ch1, ch2 := make(chan int), make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
select {
case val := <-ch1:
fmt.Println("ch1:", val) // assumes ch1 is always selected first
case val := <-ch2:
fmt.Println("ch2:", val)
}
}Impact analysis: When multiple channels are ready, select chooses a case at random, which may violate expectations.
Best practice: Use explicit priority mechanisms or additional logic when a specific order is required.
select {
case val := <-ch1:
fmt.Println("ch1:", val)
case val := <-ch2:
fmt.Println("ch2:", val)
default:
fmt.Println("no data ready")
}5. Using Improper Notification Channels
Incorrect example:
package main
func main() {
done := make(chan bool)
go func() {
// Wrongly using a bool channel for notification
done <- true
}()
<-done
}Best practice: Use a chan struct{} for signaling when the transmitted value carries no data.
package main
func main() {
done := make(chan struct{})
go func() {
// Correctly using struct{} as a notification channel
close(done)
}()
<-done
}6. Using Nil Channels Incorrectly
Incorrect example:
package main
func main() {
var ch chan int
select {
case <-ch: // uninitialized channel causes a runtime panic
}
}Best practice: Assign a nil channel to deliberately disable a select case.
func main() {
var ch chan int = nil
select {
case <-ch: // this case will never be chosen
default:
fmt.Println("branch disabled")
}
}7. Unclear Channel Buffer Size
Incorrect example:
package main
func main() {
ch := make(chan int, 0) // unbuffered channel may cause deadlock
ch <- 1
}Best practice: Choose an appropriate buffer size based on the application scenario.
ch := make(chan int, 1) // buffered channel avoids blocking8. Improper Use of append Leading to Data Races
Incorrect example:
package main
func main() {
slice := []int{}
go func() { slice = append(slice, 1) }()
go func() { slice = append(slice, 2) }()
}Impact analysis: Concurrently modifying a shared slice without synchronization causes data races.
Best practice: Protect shared data with a mutex or use a thread‑safe structure.
import "sync"
func main() {
var mu sync.Mutex
slice := []int{}
go func() {
mu.Lock()
defer mu.Unlock()
slice = append(slice, 1)
}()
// Additional goroutine(s) would also lock before appending
}Overall, concurrency and data handling are core to Go, but they hide many traps. By correctly managing context cancellation, goroutine lifecycles, channel usage, and synchronization, developers can avoid performance problems and bugs, resulting in more robust and efficient Go code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
