Fundamentals 20 min read

Why Does This Go Code Block? Uncovering Channel and Select Pitfalls

This article analyzes a Go program that deadlocks due to misuse of unbuffered channels and select, explains the underlying behavior of channels, blocking conditions, and select semantics, and provides a simple fix by buffering the stop channel while also covering Go's CSP roots and best‑practice guidelines.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Why Does This Go Code Block? Uncovering Channel and Select Pitfalls

Problem Statement

A developer posted a Go snippet that unexpectedly blocks, sparking a week‑long discussion about channel semantics, blocking behavior, and select mechanics.

Code Example

func main() {
    testContinue()
}

func testContinue() {
    in := make(chan *Content, 20)
    audit := make(chan *Content, 20)
    streamTextPreProcessStop := make(chan struct{})
    // Producer: push 2000 items into <code>in</code>
    go func() {
        for i := 0; i < 2000; i++ {
            in <- &Content{i: i}
            log.Infof("put in content = %s", strconv.Itoa(i))
        }
    }()
    // Auditor: read from <code>audit</code>, sleep 30 ms, signal stop at i==30
    go func() {
        for {
            select {
            case content, ok := <-audit:
                if !ok {
                    log.Infof("audit get in not ok")
                }
                time.Sleep(30 * time.Millisecond)
                if content.i == 30 {
                    log.Infof("audit streamTextPreProcessStop before")
                    streamTextPreProcessStop <- struct{}{}
                    log.Infof("audit streamTextPreProcessStop after")
                }
            }
        }
    }()
    // Main loop: select on stop signal or incoming data
    for {
        select {
        case <-streamTextPreProcessStop:
            log.Infof("get streamTextPreProcessStop")
            waitTimes := 0
            for {
                if waitTimes > 50 {
                    break
                }
                waitTimes++
                time.Sleep(100 * time.Millisecond)
            }
            continue
        case content, ok := <-in:
            if !ok {
                log.Infof("get in not ok")
            }
            log.Infof("get in content = %s", strconv.Itoa(content.i))
            log.Infof("audit in before content = %s", strconv.Itoa(content.i))
            audit <- content
            log.Infof("audit in after content = %s", strconv.Itoa(content.i))
        }
    }
}

Analysis of the Logic

Three channels are created: in (buffered, size 20) for raw data, audit (buffered, size 20) for items awaiting audit, and streamTextPreProcessStop (unbuffered) used as a failure signal.

A producer goroutine continuously writes 2000 Content objects into in.

An auditor goroutine reads from audit, sleeps 30 ms per item, and when i == 30 sends an empty struct on streamTextPreProcessStop.

The main loop uses select to either handle the stop signal or forward data from in to audit.

Root Cause of the Deadlock

The stop channel streamTextPreProcessStop is unbuffered. When the auditor sends the signal, the main loop may be busy consuming from in. Because the send blocks until a receiver is ready, the auditor goroutine stalls. Meanwhile the main loop continues pulling from in and forwarding to audit. The auditor is still sleeping, so audit eventually fills up and blocks, causing the whole pipeline to deadlock.

Key Concepts Explained

Unbuffered (synchronous) channels

An unbuffered channel has no storage; a send blocks until another goroutine receives, and a receive blocks until a sender is ready.

When a channel blocks

Sending blocks when the buffer is full (or when there is no buffer), and receiving blocks when the buffer is empty (or when there is no buffer). A nil channel blocks forever for both operations.

Select semantics

select

waits until at least one of its cases can proceed. If multiple cases are ready, one is chosen at random. A case that is blocked prevents the whole select from proceeding until the channel’s state changes.

Simple Fix

Make the stop channel buffered (capacity 1) so the auditor can send the signal without waiting for the main loop to be ready:

streamTextPreProcessStop := make(chan struct{}, 1)

This eliminates the deadlock because the signal is stored until the main loop processes it.

Deeper Dive into Go Channels

Go’s concurrency model is based on CSP (Communicating Sequential Processes). Channels are implemented as a ring buffer ( hchan) with fields such as qcount, dataqsiz, sendq, recvq, and a mutex to guarantee that only one goroutine reads or writes at a time, making channels concurrency‑safe.

When a goroutine blocks on a channel, it is placed in either the send‑wait queue or the receive‑wait queue. The opposite operation wakes the blocked goroutine. Only one of these queues is typically non‑empty; the exception occurs when a single goroutine uses select to both send and receive on the same channel, ending up in both queues.

Closing a channel is the sender’s responsibility. Receivers should never close a channel, and a channel with multiple concurrent senders should not be closed arbitrarily because the sender that closes may not know the state of the others.

Unbuffered channels can cause resource leakage if a goroutine remains blocked forever; the Go runtime’s garbage collector cannot reclaim such goroutines, leading to memory and goroutine leaks.

Best Practices

Prefer buffered channels for signaling when the sender may outpace the receiver.

Never close a channel from the receiver side, and avoid closing a channel that has multiple active senders.

Use select with a default case or a timeout to prevent indefinite blocking.

Combine sync.WaitGroup or errgroup with proper channel closing to coordinate graceful shutdown.

References

https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8

https://geektutu.com/post/hpg-exit-goroutine.html

《Go专家编程》

《深入理解Go并发编程》

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.

concurrencydeadlockGoselectGoroutineChannelbuffered channel
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

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.