In this tutorial we will explain everything about buffered and unbuffered channel in Golang with some practical examples.
1. What is Buffered Channel in Golang?
A buffered channel is a channel with a specified buffer capacity. The capacity, or size of the buffer, determines how many elements can be sent to the channel without the send operations blocking. When the buffer is full, subsequent sends block until another goroutine reads from the channel, freeing up space in the buffer. Similarly, if the buffer is empty, reads block until a goroutine writes data to the channel.
This buffering ability allows goroutines to communicate asynchronously: the sending goroutine is not immediately blocked from executing further instructions until the receiving goroutine is ready to receive the data, as long as there is room in the buffer. This can lead to more efficient utilization of goroutines, particularly in scenarios where the sender and receiver operate at different rates or when dealing with bursts of data.
Let's consider a simple example to illustrate the use of a buffered channel in Go. In this example, we'll have a producer that sends numbers into a buffered channel and a consumer that receives these numbers from the channel.
package main
import (
"fmt"
"time"
)
func main() {
// Creating a buffered channel with a capacity of 3
channel := make(chan int, 3)
// Producer goroutine
go func() {
for i := 1; i <= 5; i++ {
channel <- i // Send number to channel
fmt.Println("Produced:", i)
}
close(channel) // Close the channel when done producing
}()
// Consumer goroutine
go func() {
for num := range channel {
fmt.Println("Consumed:", num)
time.Sleep(time.Second) // Simulate time-consuming task
}
}()
// Wait for a moment before exiting
time.Sleep(6 * time.Second)
}
In this example:
- We create a buffered channel with a capacity of 3. This means it can hold up to 3 integers before blocking.
- The producer goroutine sends integers from 1 to 5 into the channel. The first three sends are non-blocking because the channel can hold three items. However, the fourth send will block until the consumer starts receiving data.
- The consumer goroutine receives integers from the channel and prints them out. It waits for a second after each number to simulate a time-consuming process.
- We use
time.Sleep
in the main function to prevent the program from exiting immediately, allowing time for the producer and consumer to process the data.
2. How Buffered Channel Works?
To explain how a buffered channel works in simple English, let's use the analogy of sending letters through a mailbox, as illustrated in the image.
Imagine you have two people, Alice and Bob. Alice needs to send several letters to Bob. They use a mailbox for this exchange.
In the world of Golang, this mailbox represents a "buffered channel."
- Alice (Sender): She represents a goroutine that sends data. Alice can put multiple letters (data) into the mailbox at her own pace, without waiting for Bob to take them out. This is because the mailbox (buffered channel) has space to hold several letters.
- The Mailbox (Buffered Channel): This mailbox is not just a slot but has a storage capacity. It can hold several letters at a time. This capacity allows Alice to keep sending letters without waiting for Bob to pick up each one immediately.
- Bob (Receiver): Bob represents another goroutine that receives data. He can come to the mailbox at his own time and take out the letters. He doesn't need to be there as Alice sends each letter.
In summary, in Golang:
- A channel is like a mailbox for passing messages between different parts of a program.
- An unbuffered channel is a one-message-at-a-time mailbox, where the sender must wait for the receiver to get each message before sending another.
- A buffered channel is a larger mailbox, allowing the sender to put in multiple messages without waiting, as long as there's space in the mailbox.
2.1 Blocking and Non-Blocking Operations
The concepts of blocking and non-blocking operations are central to understanding buffered channels.
- Blocking Operation: This occurs when a goroutine tries to send data to a channel but the channel is already full (its capacity is reached). In such a case, the goroutine is blocked, or in other words, it has to wait until there is space available in the channel. Similarly, if a goroutine tries to receive data from an empty channel, it will also be blocked until there is data to receive.
- Non-Blocking Operation: This happens when sending or receiving operations on a channel do not have to wait. For example, sending data to a buffered channel that hasn’t reached its capacity is a non-blocking operation, as the goroutine doesn’t have to wait and can continue executing the next lines of code.
To illustrate these concepts, let’s consider a simple Go program that uses a buffered channel.
package main
import (
"fmt"
"time"
)
func main() {
// Creating a buffered channel of integers with a capacity of 3
channel := make(chan int, 3)
// This is a non-blocking operation
channel <- 1
channel <- 2
channel <- 3
// This would be a blocking operation if uncommented
// channel <- 4 // blocks here since the channel is full
go func() {
// Receiving data from the channel
fmt.Println(<-channel) // 1
fmt.Println(<-channel) // 2
fmt.Println(<-channel) // 3
// If we try to receive more, it will block as the channel is empty
}()
time.Sleep(time.Second)
}
In this example:
- We create a buffered channel with a capacity of 3.
- We send three integers to the channel, which are non-blocking operations since the channel isn't full.
- If we uncomment the line
channel <- 4
, the program will block because the channel's capacity is full. - A separate goroutine is then used to receive the integers from the channel. If it tries to receive more data than what is available, it will block.
This example demonstrates the behavior of blocking and non-blocking operations in the context of buffered channels in Golang, providing a practical understanding of how they manage inter-goroutine communication and concurrency.
3. Advantages of Buffered Channels
Buffered channels in Golang offer several advantages over their unbuffered counterparts, particularly in scenarios involving concurrent operations. Here's a detailed look at these benefits:
3.1 Asynchronous Communication
Buffered channels enable asynchronous data exchange between goroutines. This means that a goroutine can continue executing its code without having to wait for another goroutine to receive the data it has sent. This feature is particularly useful in situations where you want to maintain a smooth flow of data between different parts of your program.
3.2 Reduced Synchronization Overhead
In concurrent programming, managing the timing and interaction between threads (or goroutines in Golang) can be complex and costly in terms of performance. Buffered channels reduce the need for tight synchronization between goroutines, as they allow for temporary storage of data. This reduction in synchronization overhead can lead to more efficient and simpler code.
3.3 Potential Performance Improvements
Buffered channels can potentially improve the performance of a Go program. By allowing goroutines to send and receive data without immediate synchronization, you can reduce the waiting time that often occurs in tightly coupled concurrent processes. This can be particularly advantageous in high-load scenarios where you're dealing with a lot of data or many concurrent operations.
Let's consider an example to illustrate the advantages of buffered channels:
package main
import (
"fmt"
"time"
)
func main() {
// Creating a buffered channel with a capacity of 2
dataChannel := make(chan int, 2)
go producer(dataChannel)
go consumer(dataChannel)
// Simulate work for 1 second before exiting
time.Sleep(1 * time.Second)
}
// producer sends data to the channel
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i // Non-blocking for the first 2 elements
fmt.Println("Produced:", i)
}
}
// consumer receives data from the channel
func consumer(ch chan int) {
for {
data := <-ch
fmt.Println("Consumed:", data)
time.Sleep(500 * time.Millisecond) // simulate time-consuming task
}
}
In this example:
- The
producer
function sends data to a buffered channel. The first two sends are non-blocking because the channel can hold two items. This demonstrates asynchronous communication. - The
consumer
function receives data from the channel. The buffered channel allows the consumer to take its time processing data without forcing the producer to wait, showcasing reduced synchronization overhead. - The overall flow of the program is smoother and potentially more efficient because the producer doesn't block while the consumer is processing, illustrating potential performance improvements.
4. Comparing Buffered and Unbuffered Channels
Buffered and unbuffered channels in Golang serve the same fundamental purpose: they facilitate communication between goroutines. However, their behaviors, use cases, and performance implications differ significantly.
4.1 Behavior Differences
Unbuffered Channels:
- Act as a direct line between sender and receiver.
- Every send operation (
chan <-
) must be paired with a corresponding receive operation (<- chan
) and vice versa. - They are synchronous, meaning the send operation must wait until the receive operation is ready.
Buffered Channels:
- Provide a buffer between sender and receiver.
- Allow send operations to proceed without waiting for a receive operation, as long as the buffer is not full.
- They are asynchronous to an extent, as they do not require immediate pairing of send and receive operations.
4.2 Use Cases
Unbuffered Channels:
- Ideal for ensuring strong synchronization between goroutines, where each message sent is immediately handled.
- Used when you need to guarantee that a goroutine has completed its task before another starts.
Buffered Channels:
- Suitable for scenarios where you need a certain level of independence between sending and receiving operations.
- Useful when dealing with bursts of data or when sender and receiver operate at different rates.
4.3 Performance Implications
Unbuffered Channels:
- Can lead to more predictable and safer code, as each send operation is directly handled by a corresponding receive operation.
- May cause the program to run slower in scenarios where goroutines are frequently blocked waiting for other goroutines.
Buffered Channels:
- Can improve overall program performance by allowing goroutines to continue executing without waiting for direct synchronization.
- However, improper use (like setting an inappropriately large buffer size) can lead to memory inefficiencies or complexities in handling channel state.
Example with Unbuffered Channel:
package main
import "fmt"
func main() {
messages := make(chan string) // Unbuffered channel
go func() {
messages <- "ping"
}()
msg := <-messages
fmt.Println(msg) // Output: ping
}
Here, the sending goroutine waits for the main
goroutine to be ready to receive the message. This ensures direct synchronization.
Example with Buffered Channel:
package main
import "fmt"
func main() {
messages := make(chan string, 2) // Buffered channel
messages <- "buffered"
messages <- "channel"
fmt.Println(<-messages) // Output: buffered
fmt.Println(<-messages) // Output: channel
}
In this case, the main
goroutine does not have to be ready to receive when the messages are sent. The channel buffers up to two messages, allowing the sender to proceed without immediate receiver readiness.
5. Summary
Golang Buffered channels are used for communicating between concurrently running functions by sending and receiving a specific element type’s data for a specific capacity. Executing a numerous Goroutines running at the same time the pipeline of communication is controlled by the capacity to avoid blocking, if the channel is full it blocks the channel pipeline, until receiving is done.
You can read more at concurrency and channel for further information.