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.

FunTester
FunTester
FunTester
7 Common Go Concurrency Pitfalls and How to Avoid Them

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 blocking

8. 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyGobest practicesGoroutinecontextChannel
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.