Understanding Go Channels: Unbuffered vs Buffered, Usage, and Common Patterns
This article explains Go's channel primitive, comparing unbuffered and buffered channels, demonstrates how to avoid deadlocks with proper goroutine coordination, and shows typical patterns such as select, closing channels, and building concurrency controllers for robust backend services.
Go is a language well‑suited for building high‑concurrency services, and goroutine is its lightweight concurrency primitive; the core challenge in Go concurrency is how goroutines communicate.
The Go community advises "don't communicate by sharing memory; share memory by communicating". Channels provide an elegant way to exchange data between goroutines without explicit locks.
Channel Basics
A channel is a dedicated memory area that can hold one or more values of a specified type. Sending blocks when the channel is full; receiving blocks when the channel is empty.
Unbuffered Channels
Unbuffered (synchronous) channels require a sender and a receiver to be ready simultaneously. They are created with make(chan int) and used with the <- operator.
c1 := make(chan int) // int channel
c2 := make(chan string) // string channel
c1 <- 1 // send
ch := <-c1 // receiveA classic deadlock occurs when the main goroutine sends on an unbuffered channel before a receiver is ready, causing both to block. The solution is to run the sender or receiver in a separate goroutine.
Buffered Channels
Buffered channels allow asynchronous communication; they are created with a capacity, e.g., make(chan string, 10) . Sending only blocks when the buffer is full, and receiving only blocks when the buffer is empty.
c1 := make(chan string, 10) // buffered channel
c1 <- "hello" // send
ch := <-c1 // receiveThe article compares the two types in a table, highlighting that unbuffered channels can hold only one value and require both parties to be ready, while buffered channels can hold multiple values and allow asynchronous exchange.
Channel Operations
Both channel types must be created with make . Nil channels block on send/receive and panic on close.
Channels can be closed; after closing, sends panic but receives continue, returning the zero value and a false ok flag.
The select statement can multiplex multiple channel operations, with an optional default case when none are ready.
Typical Applications
Concurrent task processing: launch many goroutines to fetch data, collect results via a channel, and use select with time.After to implement a simple circuit‑breaker for unreliable external calls.
Concurrency control: a custom controller limits the number of simultaneous goroutines using a mutex, counters, and channels ( addJob , finishJob ) to queue excess tasks.
type ConcurrentCtrl struct {
lock *sync.Mutex
maxNum int
currentNum int
addJob chan *jobNode
finishJob chan bool
}
func NewConcurrentCtrl(maxNum int) *ConcurrentCtrl { /* … */ }
func (p *ConcurrentCtrl) startMaster() { /* select on addJob and finishJob */ }This controller pattern can also be adapted for connection pools or long‑living resources.
In summary, unbuffered channels behave like a phone call where both parties must be ready, while buffered channels act like a mailbox where the sender does not need to wait for the receiver, making them suitable for a wide range of backend concurrency scenarios.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.