Unbuffered vs Buffered Channels in Go

2024-11-11
....

Channels

In Go, we can think of channels as tunnels that allow goroutines to communicate and synchronize with each other by passing data.

There are two types of channels in Go: unbuffered and buffered channels. Let's see what they are and how they differ from each other.

Unbuffered Channel

An unbuffered channel in Go is a channel with no capacity to store any data. Any value sent to an unbuffered channel must be immediately handed off to a receiving goroutine, meaning the send and receive operation must occur simultaneously.

Let's see an example:

main.go
import (
    "fmt"
    "sync"
)
 
func main() {
    var wg sync.WaitGroup
    ch := make(chan int) // unbuffered channel
    wg.Add(1)
 
    go func() {
        defer wg.Done()
        for val := range ch {
            fmt.Println(val)
        }
    }()
 
    ch <- 1 // blocks until received
    ch <- 2 // blocks until received
    close(ch) // closing the channel after all sends
    wg.Wait() 
}

Here, ch <- 1 operation blocks until the receiving end is ready to receive the value. Similarly, ch <- 2 blocks until the receiving end is ready to receive the second value.

Buffered Channel

A buffered channel in Go is a channel that has a specified capacity to hold a set number of values. This means a buffered channel can temporarily store values without requiring an immediate receiver. Unlike unbuffered channels, send and receive operations on buffered channels don't have to happen at the same time as long as the buffer isn't full.

Let's see an example:

main.go
package main
 
import "fmt"
 
func main() {
	ch := make(chan int, 2) // buffered channel with capacity 2
 
	ch <- 1 // does not block
	ch <- 2 // does not block
 
  // this line would cause a block because the buffer is full
  // ch <- 3 
 
	fmt.Println(<-ch) // receives 1
	fmt.Println(<-ch) // receives 2
}

Here, sending 1 and 2 to the channel doesn't block because the buffer has enough capacity for both values.

To handle the blocking of ch <-3 when the buffer is full, we can introduce a goroutine to continuously receive values from the channel, which frees up space in the buffer and prevents it from blocking. Here's how:

main.go
package main
 
import "fmt"
 
func main() {
	ch := make(chan int, 2) // buffered channel with capacity 2
 
	// start a goroutine that continuously reads from the channel
	go func() {
		for val := range ch { 
			fmt.Println(val) 
		}
	}()
 
	ch <- 1 // does not block 
	ch <- 2 // does not block 
	ch <- 3 // does not block, as the goroutine is already receiving values
 
	close(ch) // close the channel to indicate no more values will be sent
}
 

Thank you for reading!

© Shaunak 2024. All Rights Reserved.