Unbuffered Channels
Unbuffered channels are the default type of channel in Go. They provide synchronous communication between goroutines, meaning that a send operation will block until there's a corresponding receive operation ready to receive the value, and vice versa.
Creating an Unbuffered Channel#
To create an unbuffered channel, we use the make function without specifying a buffer size:
string">"comment">// Create an unbuffered channel of integersch := make(chan int)
Understanding Synchronization#
The key characteristic of unbuffered channels is that they synchronize the sender and receiver:
- A send operation
ch <- valueblocks until another goroutine is ready to receive from the channel - A receive operation
value := <-chblocks until another goroutine sends a value to the channel
This means that the sender and receiver must "meet" in time for the communication to take place.
Visual Representation#
Here's a visual representation of how unbuffered channels work:
Sender Receiver
| |
| --- send value ------> | (blocks until receiver is ready)
| |
| <--- acknowledged <--- |
| |
Basic Example#
Let's look at a simple example of using an unbuffered channel:
package mainimport (string">"fmt"string">"time")func main() {string">"comment">// Create an unbuffered channelch := make(chan string)string">"comment">// Start a goroutine that sends a messagego func() {fmt.Println(string">"Sending message...")ch <- string">"Hello!" string">"comment">// This will block until the main goroutine receivesfmt.Println(string">"Message sent!")}()string">"comment">// Wait a moment to demonstrate the blocking behaviortime.Sleep(2 * time.Second)string">"comment">// Receive the messagefmt.Println(string">"About to receive...")msg := <-ch string">"comment">// This unblocks the sending goroutinefmt.Println(string">"Received:", msg)string">"comment">// Allow time to see the string">"Message sent!" outputtime.Sleep(time.Millisecond)}
When you run this program, you'll see:
Sending message...
About to receive...
Received: Hello!
Message sent!
Notice the sequence of events:
- The goroutine starts and prints "Sending message..."
- It tries to send "Hello!" but blocks because no one is receiving yet
- After 2 seconds, the main goroutine prints "About to receive..."
- It receives the message, allowing the goroutine to continue
- The goroutine prints "Message sent!" and exits
Multiple Senders and Receivers#
Unbuffered channels work well with multiple senders and receivers:
package mainimport (string">"fmt"string">"sync"string">"time")func sender(id int, ch chan<- string, wg *sync.WaitGroup) {defer wg.Done()msg := fmt.Sprintf(string">"Message from sender %d", id)fmt.Printf(string">"Sender %d: sending message\n", id)ch <- msg string">"comment">// Will block until a receiver is readyfmt.Printf(string">"Sender %d: message sent\n", id)}func receiver(id int, ch <-chan string, wg *sync.WaitGroup) {defer wg.Done()string">"comment">// Simulate different readiness timestime.Sleep(time.Duration(id) * 100 * time.Millisecond)fmt.Printf(string">"Receiver %d: waiting for message\n", id)msg := <-ch string">"comment">// Will block until a sender is readyfmt.Printf(string">"Receiver %d: got message: %s\n", id, msg)}func main() {ch := make(chan string) string">"comment">// Unbuffered channelvar wg sync.WaitGroupstring">"comment">// Start 3 sendersfor i := 1; i <= 3; i++ {wg.Add(1)go sender(i, ch, &wg)}string">"comment">// Start 3 receiversfor i := 1; i <= 3; i++ {wg.Add(1)go receiver(i, ch, &wg)}string">"comment">// Wait for all goroutines to finishwg.Wait()}
In this example, senders and receivers pair up one-to-one. The order of pairing depends on scheduling and timing.
Use Cases for Unbuffered Channels#
Unbuffered channels are ideal for:
- Synchronization: When you need to synchronize the execution of goroutines
- Guaranteed delivery: When you need to ensure that a message is received
- Signal events: When you want to signal that an event has occurred
- Turn-taking: When goroutines need to take turns performing operations
Signaling with Unbuffered Channels#
A common pattern is to use unbuffered channels for signaling, often with empty structs to minimize memory usage:
package mainimport (string">"fmt"string">"time")func worker(done chan struct{}) {fmt.Println(string">"Working...")time.Sleep(time.Second)fmt.Println(string">"Done working!")string">"comment">// Signal that we're donedone <- struct{}{}}func main() {done := make(chan struct{})go worker(done)string">"comment">// Wait for the worker to signal completion<-donefmt.Println(string">"Main: received completion signal")}
Deadlocks with Unbuffered Channels#
A common mistake with unbuffered channels is creating situations that lead to deadlocks:
package mainfunc main() {ch := make(chan int)string">"comment">// This will deadlock because there's no receiverch <- 1string">"comment">// Code here will never execute<-ch}
If you run this, you'll get:
fatal error: all goroutines are asleep - deadlock!
To fix this, you need to ensure that sends and receives happen in separate goroutines:
package mainimport string">"fmt"func main() {ch := make(chan int)string">"comment">// Send in a separate goroutinego func() {ch <- 1}()string">"comment">// Now we can receivevalue := <-chfmt.Println(value)}
Nil Channels#
It's important to note that a nil channel (the zero value of a channel variable) will block forever on both send and receive operations:
var ch chan int string">"comment">// nil channel<-ch string">"comment">// blocks foreverch <- 1 string">"comment">// blocks forever
This behavior can be useful in select statements to disable specific cases.
Channel Closing#
When you close an unbuffered channel, any blocked senders will panic, but blocked receivers will get the zero value of the channel's type with an ok value of false:
ch := make(chan int)close(ch)string">"comment">// This will return 0, falsevalue, ok := <-chstring">"comment">// This will range over 0 values and exit immediatelyfor v := range ch {string">"comment">// Never executed}
Performance Considerations#
Unbuffered channels are generally very fast, but there's a performance cost to the context switching and synchronization. If performance is critical and you don't need the synchronization guarantees, buffered channels might be more appropriate.
Summary#
- Unbuffered channels provide synchronous communication between goroutines
- They block until both sender and receiver are ready
- They're useful for synchronization and guaranteed delivery
- Be careful about potential deadlocks
- Nil channels block forever
- Closing channels unblocks receivers but causes senders to panic
Unbuffered channels are one of the most powerful synchronization tools in Go. Understanding their behavior is essential for writing correct and efficient concurrent programs.
In the next lab, we'll compare unbuffered and buffered channels to understand when to use each type.