Table of Contents
We have already covered golang channel in depth in one of our previous articles. In this article, we will be discussing how to implement a Golang buffered channel with help of a practical example.
What is a buffered channel in GO?
A buffered channel is a type of channel that has storage capacity within. This type of channel allows you to specify a fixed length of buffer capacity so one can send that number of data values at once. Sending and receiving data are only blocked when the buffer is full. The length of the buffered channel is the number of data in the queue.
The channel is created using make
as follows:-
ch := make(chan type, capacity)
How buffered channel works?
For an example, consider this buffered channel with 3 blocks as capacity.
A send operation on a buffered channel inserts an element at the back of the queue, and a receive operation removes an element from the front. If the channel is full, the send operation blocks its goroutine until space is made available by another goroutine’s receive. Conversely, if the channel is empty, a receive operation blocks until a value is sent by another goroutine.
We can send up to three values on this channel without the goroutine blocking:
ch <- "A"
ch <- "B"
ch <- "C"
At this point, the channel is full, and a fourth send statement would block.
If we receive one value:
fmt.Println(<-ch) // "A"
the channel is neither full nor empty, so either a send operation or a receive operation could proceed without blocking. In this way, the channel’s buffer decouples the sending and receiving goroutines.
After two more receive operations the channel is empty again, and a fourth would block:
fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"
Reading a slice of string using buffered channel
We can use buffered channel to send data without using goroutine. as follows
package main
import (
"fmt"
)
var data = []string{"Hello", "Go", "Linux"}
func sendData(ch chan string) {
for _, elem := range data {
ch <- elem
}
close(ch)
}
func main() {
// creates capacity of 2
ch := make(chan string, 2)
sendData(ch)
for v := range ch {
fmt.Println(v)
}
}
Output without using goroutine:
it will fail since the data length exceed the desire buffer capacity of 2, thus blocked channel. If we use goroutine it will execute continuously without realizing that issue. go sendData(ch)
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.sendData(...)
/tmp/sandbox3578502812/prog.go:11
main.main()
/tmp/sandbox3578502812/prog.go:19 +0x105
Go playground_without goroutine
Output with using goroutine:
$ go run main.go
Hello
Go
Linux
Buffered channel deadlock
A deadlock is a situation where data cant be send or received on a channel. This occurs when the data send exceeds the defined buffer capacity. Buffered channel holds data for defined capacity, in an event where data being send exceeds the capacity limit, the channel is blocked, hence deadlock state. For example:-
package main
import "fmt"
func main() {
// ch:= make(chan type, capacity)
dataChannel := make(chan string, 2)
// sending data to the channel
dataChannel <- "Hello"
dataChannel <- "Go"
dataChannel <- "Linux"
close(dataChannel)
// Receiving data
fmt.Println("Data received:")
for value := range dataChannel {
fmt.Printf("%v \n", value)
}
}
Output:
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox2046190718/prog.go:12 +0x5c
Lock and Unlock with buffered channels
The sync package provides sync.Locker and sync.Mutex with locking features to lock sensitive areas of codes. This is how locking feature works
- A function acquires a lock by sending a message on a channel.
- The function proceeds to do its sensitive operations.
- The function releases the lock by reading the message back off the channel.
- Any function that tries to acquire the lock before it’s been released will pause when it tries to acquire the (already locked) lock.
We can not implement this scenario with an unbuffered channel. The first step in this process would cause the function to block because an unbuffered channel blocks on send. In other words, the sender waits until something receives the message it puts on the channel.
But one of the features of a buffered channel is that it doesn’t block on send provided that buffer space still exists. A sender can send a message into the buffer and then move on. But if a buffer is full, the sender will block until there’s room in the buffer for it to write its message.
package main
import (
"fmt"
"time"
)
func main() {
// create buffered channel with 1 space
lock := make(chan bool, 1)
for i := 1; i < 7; i++ {
// start up to 6 goroutines
// sharing the locking channel
go worker(i, lock)
}
time.Sleep(10 * time.Second)
}
func worker(id int, lock chan bool) {
fmt.Printf("%d wants the lock\n", id)
// A worker acquires the lock by sending
// it a message. The first worker to hit
// this will get the one space and this
// own the lock. The rest will block
lock <- true
fmt.Printf("%d has the lock\n", id)
// The space between the lock <- true
// and the <- lock is "locked"
time.Sleep(500 * time.Millisecond)
fmt.Printf("%d is releasing the lock\n", id)
// Release the lock by reading a value,
// which then opens that one space on the buffer
// again so that the next function can lock it
<-lock
}
This pattern is simple: there’s one step to lock and one to unlock. If you run this program, the output will look like this:
$ go run main.go
3 wants the lock
3 has the lock
2 wants the lock
5 wants the lock
6 wants the lock
4 wants the lock
1 wants the lock
[T+0000ms]
3 is releasing the lock
2 has the lock
[T+0500ms]
2 is releasing the lock
5 has the lock
[T+0500ms]
5 is releasing the lock
6 has the lock
[T+0500ms]
6 is releasing the lock
4 has the lock
[T+0500ms]
4 is releasing the lock
1 has the lock
[T+0500ms]
Explanation: In this output, you can see how your six goroutines sequentially acquire and release the lock. Within the first few milliseconds of starting the program, all six goroutines have tried to get the lock. But only goroutine 3
gets it. A few hundred milliseconds later, 3
releases the lock and 2
gets it. And the lock trading continues until the last goroutine (1
) acquires and releases the lock.
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.
References