Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

An unbuffered channel in Go is a typed conduit for synchronous communication between goroutines, characterized by having zero capacity to store data. Because it lacks an internal buffer, a send operation on an unbuffered channel blocks the sending goroutine until another goroutine executes a corresponding receive operation on the same channel, and vice versa. This creates a strict rendezvous point where data is handed off directly from the sender to the receiver.

Syntax

An unbuffered channel is instantiated using the built-in make function without specifying a capacity argument, or by explicitly setting the capacity to 0.
// Idiomatic declaration of an unbuffered channel
ch := make(chan int)

// Functionally identical, explicitly defining 0 capacity
ch2 := make(chan int, 0)

Execution Mechanics

The defining characteristic of an unbuffered channel is its strict synchronization guarantee. The Go runtime manages this through the scheduler:
  1. Send Operation (ch <- data): When a goroutine attempts to send data into an unbuffered channel, the Go scheduler pauses that goroutine. It remains in a waiting state until a different goroutine executes a receive operation on the exact same channel.
  2. Receive Operation (<-ch): Conversely, if a goroutine attempts to read from an unbuffered channel before any data has been sent, the scheduler pauses the receiving goroutine until a sender becomes available.
  3. The Handoff: Once both a sender and a receiver are ready at the same time, the data is copied directly from the sending goroutine’s memory stack to the receiving goroutine’s memory stack. No intermediate queuing or buffering occurs.

Visualization of Blocking Behavior

To prevent the main goroutine from exiting before the receiving goroutine completes its execution, a secondary synchronization mechanism (like a done channel) is required.
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	done := make(chan struct{}) // Synchronization channel to prevent premature exit

	go func() {
		fmt.Println("Receiver: Waiting for data...")
		// Execution halts here until the main goroutine sends data
		val := <-ch 
		fmt.Println("Receiver: Got", val)
		close(done) // Signal that the receiver has finished processing
	}()

	time.Sleep(2 * time.Second) // Simulate work in the main goroutine

	fmt.Println("Sender: Sending data...")
	// Execution halts here until the anonymous goroutine is ready to receive
	// (In this specific flow, the receiver is already waiting)
	ch <- "signal" 
	fmt.Println("Sender: Data sent.")

	<-done // Block main until the receiver signals completion
}

Deadlock Conditions

Because unbuffered channels require simultaneous participation from both a sender and a receiver, they are highly susceptible to deadlocks if the complementary operation is never executed. If a single goroutine attempts to send to or receive from an unbuffered channel without another concurrent goroutine available to fulfill the opposite side of the transaction, the operation will block indefinitely. If the Go runtime detects that all active goroutines are asleep and waiting on channel operations, it will crash the program with a fatal error: all goroutines are asleep - deadlock! panic.
package main

func main() {
	ch := make(chan int)
    
	// This will cause a fatal deadlock panic.
	// The main goroutine blocks on the send operation, 
	// but no other goroutine exists to receive the value.
	ch <- 42 
}
Master Go with Deep Grasping Methodology!Learn More