Backend Development 10 min read

Common Concurrency Bugs in Go and Their Fixes

Analyzing several major Go projects, the article catalogs frequent concurrency pitfalls—such as unbuffered channels, WaitGroup deadlocks, context leaks, loop‑variable races, double channel closes, timer errors, and RWMutex misuse—and offers concise, idiomatic fixes to prevent blocking, leaks, panics, and deadlocks.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Common Concurrency Bugs in Go and Their Fixes

Go encourages concurrent programming through goroutines and channels. By analyzing commit logs of six major open‑source projects (Docker, Kubernetes, etc.), the article identifies typical concurrency bugs and provides practical fixes.

01 Unbuffered channel causing sender block

The following function creates an unbuffered channel and blocks on send when a timeout occurs because the receiver has already exited.

func finishReq(timeout time.Duration) ob {
    ch := make(chan ob)
    go func() {
        result := fn()
        ch <- result // block
    }()
    select {
    case result = <-ch:
        return result
    case <-time.After(timeout):
        return nil
    }
}

Fix: make the channel buffered so the send does not block.

func finishReq(timeout time.Duration) ob {
    ch := make(chan ob, 1)
    go func() {
        result := fn()
        ch <- result // no block
    }()
    select {
    case result = <-ch:
        return result
    case <-time.After(timeout):
        return nil
    }
}

02 WaitGroup misuse leading to deadlock

Calling group.Wait() inside the loop creates a situation where only one goroutine calls Done() , causing the wait to block forever.

var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {
    go func(p *plugin) {
        defer group.Done()
    }(p)
    group.Wait()
}

Fix: move group.Wait() outside the loop.

var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {
    go func(p *plugin) {
        defer group.Done()
    }(p)
}
group.Wait()

03 Context misuse causing resource leak

Creating a new cancelable context and then overwriting it without calling the previous cancel leaves a goroutine running.

hctx, hcancel := context.WithCancel(ctx)
if timeout > 0 {
    hctx, hcancel = context.WithTimeout(ctx, timeout)
}

Fix: create the appropriate context once, or cancel the previous one before overwriting.

var hctx context.Context
var hcancel context.CancelFunc
if timeout > 0 {
    hctx, hcancel = context.WithTimeout(ctx, timeout)
} else {
    hctx, hcancel = context.WithCancel(ctx)
}
hctx, hcancel := context.WithCancel(ctx)
if timeout > 0 {
    hcancel()
    hctx, hcancel = context.WithTimeout(ctx, timeout)
}

04 Goroutine closure capturing loop variable

Reading the loop variable i inside a goroutine creates a data race because all goroutines share the same variable.

for i := 17; i <= 21; i++ { // write
    go func() { // read
        apiVersion := fmt.Sprintf("v1.%d", i)
    }()
}

Fix: pass the loop variable as a parameter (pass‑by‑value).

for i := 17; i <= 21; i++ { // write
    go func(i int) { // read
        apiVersion := fmt.Sprintf("v1.%d", i)
    }(i)
}

05 Multiple close of a channel

Concurrent execution of a select that closes a channel can close it more than once, causing a panic.

select {
case <-c.closed:
default:
    close(c.closed)
}

Fix: protect the close with sync.Once .

once.Do(func() {
    close(c.closed)
})

06 Timer misuse

Initializing a timer with zero duration makes timer.C fire immediately, defeating the intended timeout logic.

timer := time.NewTimer(0)
if dur > 0 {
    timer = time.NewTimer(dur)
}
select {
case <-timer.C:
case <-ctx.Done():
    return nil
}

Fix: create the timer only when dur > 0 and use a nil channel otherwise.

var timeout <-chan time.Time
if dur > 0 {
    timeout = time.NewTimer(dur).C
}
select {
case <-timeout:
case <-ctx.Done():
    return nil
}

07 RWMutex misuse

A read lock held while another goroutine acquires a write lock can cause deadlock because the write lock blocks further readers.

Understanding that a write lock has higher priority helps avoid recursive read locking and deadlocks.

These examples, together with references to the Go language specification and effective Go guidelines, illustrate common pitfalls and their straightforward remedies.

ConcurrencyGoBugsContextChannelsRaceConditionsWaitGroup
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

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