Golang Buffered Channel Tutorial [Practical Examples]


Written By - David Musau
Advertisement

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)
NOTE:

The capacity of the buffer should be greater than 0. The value of capacity being greater than zero makes it a buffered channel because the unbuffered channel is always 0 by default and thus omitted during the declaration of a channel.

How buffered channel works?

For an example, consider this buffered channel with 3 blocks as capacity.

Golang Buffered Channel Tutorial [Practical Examples]

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.

Golang Buffered Channel Tutorial [Practical Examples]

Advertisement

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.

Golang Buffered Channel Tutorial [Practical Examples]

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

Go playground_with goroutine

 

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

Run program Go playground

 

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
}

Run program Go playground

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

concurrency
channel

 

Categories GO

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment