Unread channel
| Vulnerability potential | None |
| DDoS potential | Low |
The channel is never read
Impact
A channel is written to but no goroutine ever receives from it. On an unbuffered channel the first send blocks forever, stalling the sender. On a buffered channel, sends succeed until the buffer fills, after which every further send blocks. In both cases the sending goroutines park permanently and are never reclaimed, which is a goroutine (and memory) leak. Values placed in the channel are never consumed, so the work they represent is silently dropped. The program does not crash or error; it slowly accumulates blocked goroutines and the objects they reference, degrading over time.
Vulnerability potential
This issue has essentially no memory-safety relevance (Go is safe here) but a modest denial-of-service angle.
- If senders are spawned per request and their channel is never drained, each request leaks a blocked goroutine and its retained data; sustained traffic grows memory and goroutine count without bound until the process is killed.
- Otherwise the consequence is dropped work and wasted resources rather than a security boundary being crossed.
Technical details
Send and receive on an unbuffered channel rendezvous: a send completes only when a matching receive is ready. With no receiver, the send’s goroutine is parked on the channel’s send wait queue indefinitely. A buffered channel decouples sender and receiver up to its capacity; once full it behaves like the unbuffered case.
Buffered vs unbuffered
make(chan T) (capacity 0) blocks the very first send if unread.
make(chan T, N) absorbs N sends, then blocks. A buffer only delays, it does not
fix, a missing reader.
Common sources
A producer goroutine launched but its consumer forgotten or removed in a
refactor; a results channel returned to a caller that ignores it; a select
that no longer has a receiving case. The fix is to ensure a receiver exists for
the channel’s full lifetime, or to use a select with default/ctx.Done() so
the send cannot block indefinitely, or to not produce values nobody consumes.
Catching the issue
Goroutine leak detection
Use go.uber.org/goleak in tests to assert no goroutines are left blocked after
a test completes; an unread channel surfaces as a leaked goroutine parked in
chansend. Profile live processes with /debug/pprof/goroutine and watch
runtime.NumGoroutine() for monotonic growth.
Static analysis and review
staticcheck and channel-direction typing help, but the strongest defense is a
review rule: every channel has a clearly owned receiver, and senders use a
bounded/cancelable send (select with ctx.Done()), so a vanished consumer
fails fast instead of leaking.
How to reproduce
Observe a deadlock: the send on an unbuffered channel blocks forever because nothing ever reads it.
package main
import "fmt"
func main() {
ch := make(chan int) // unbuffered, never read
fmt.Println("sending...")
ch <- 1 // blocks forever; runtime: all goroutines asleep - deadlock
fmt.Println("unreachable")
}