Table of Contents
Golang Channel
Send & Receive Golang Channel
package main
import "fmt"
func main() {
c := make(chan int)
go foo(c)
bar(c)
fmt.Printf("Exiting...")
}
//send
func foo(c chan<- int) {
c <- 42
}
//receive
func bar(c <-chan int) {
fmt.Println(<-c)
}
Buffered Channels
- In Go, a channel with a buffer size >= 1 is known as an buffered channel.
- ie. ch := make(chan int, 3) // Buffered channel with a capacity of 3
Use Cases
- Decoupling Producer and Consumer:
- Buffered channels allow a producer to generate data independently of the consumer. The producer can continue generating data up to the buffer’s capacity without waiting for the consumer to be ready.
- Concurrency Control:
- Buffered channels can be used to limit the number of concurrently running goroutines or to control the rate at which data is processed.
- Performance Optimization:
- When performance is critical and the overhead of context switching (from blocking in unbuffered channels) needs to be minimized, buffered channels can help by allowing some level of asynchronous processing.
Advantages
- Improved Throughput: By decoupling the sender and receiver, buffered channels can improve the throughput of your system.
- Flexibility: Buffered channels provide more flexibility in designing systems where producers and consumers operate at different rates.
Example
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 3) // Buffered channel with a capacity of 3
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch) // Outputs: 1
fmt.Println(<-ch) // Outputs: 2
fmt.Println(<-ch) // Outputs: 3
}
Unbuffered Channel
- In Go, a channel with a buffer size of 0 is known as an unbuffered channel.
- The primary characteristic of an unbuffered channel is that it requires both a sender and a receiver to be ready at the same time in order for communication to occur.
- If either the sender or the receiver is not ready, the operation will block.
Use Cases
- Synchronization:
- Goroutine Coordination: Unbuffered channels are perfect for cases where you need to ensure that two goroutines are synchronized. For example, one goroutine may need to wait until another goroutine has completed its task before proceeding.
- Handshaking: If you need strict control over when data is passed between goroutines, unbuffered channels enforce that both the sender and receiver are ready at the same time.
- Pipeline Stages:
- When setting up stages in a pipeline where each stage processes data and then passes it to the next, unbuffered channels can ensure that data is processed as it arrives, without being buffered.
- Resource Management:
- If a resource should only be accessed by one goroutine at a time and you want to serialize access to that resource, an unbuffered channel can help enforce that one goroutine sends and another receives, thus managing access to the resource.
Advantages
- Strict Synchronization: Ensures that data is passed only when both sender and receiver are ready.
- Simplicity: Simple and easy to reason about when coordinating goroutines.
Example
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Println("Working...")
time.Sleep(time.Second)
fmt.Println("Done!")
done <- true // Send signal to indicate completion
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Wait for the worker to finish
}
When to Use Each
- Buffered Channels: Use when you want to decouple the timing between producers and consumers, allow for some level of buffering in your communication, or improve the performance by reducing blocking. They are suitable for tasks where the producer may generate data faster than the consumer can process it, or where the communication latency needs to be minimized.
- Unbuffered Channels: Use when you need precise synchronization between goroutines or when the order of operations between goroutines is important. They are ideal for tasks where you need to wait for another goroutine to finish before proceeding.
Real-World Scenarios
- Buffered Channels: Frequently used in scenarios like job queues, message passing between different parts of a system where rate control is needed, and situations where you want to avoid blocking the producer.
- Unbuffered Channels: Often used in situations like task completion signals, coordinating worker pools, and pipeline stages where strict order and timing control are needed.
Conclusion
Both unbuffered and buffered channels have their place in real-world Go programming. The choice depends on whether you need tight synchronization or more flexibility and decoupling in your concurrent operations. Use unbuffered channels for strict handshakes and synchronization, and buffered channels when you need to improve throughput and decouple producers and consumers.
Various Cases
Success -1
Single item processed using channel
package main
import "fmt"
func main() {
ch := make(chan int, 1)
// pushing input 15 in channel 'ch'
ch <- 15
// processing items from 'ch'
val, ok := <-ch
fmt.Println(val, ok)
fmt.Printf("main exiting\n")
}
******************************
Output
******************************
go run channel.go
15 true
main exiting
Error-1
all goroutines are asleep – deadlock!
package main
func main() {
// no channel size provided
ch := make(chan int)
// The send operation (ch <- 15) will block indefinitely,
// because there is no corresponding receive operation
// in a separate goroutine to retrieve the value from the channel.
// This will result in a deadlock and eventually cause a runtime panic
ch <- 15
// close(ch)
// ch := make(chan int, 1)
// val, ok := <-ch
// fmt.Println(val, ok)
fmt.Printf("main exiting\n")
}
******************************
Output
******************************
go run channel.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
channel.go:6 +0x34
exit status 2
Solution-1.1
- Make both producer & consumer ready for data processing
package main
import "fmt"
func main() {
// channel of size 0
ch := make(chan int)
fmt.Printf("channel size=%d\n", len(ch))
// making consumer ready
go func() {
val, ok := <-ch
fmt.Println(val, ok)
}()
ch <- 15
fmt.Println("main exiting")
}
******************************
Output
******************************
channel size=0
15 true
main exiting
Solution-1.2
- Provide size for channel ‘ch’ e.g. 1
package main
func main() {
ch := make(chan int, 1)
ch <- 1
}
******************************
Output
******************************
go run channel.go
<no error>
Channel Range
package main
import "fmt"
func main() {
c := make(chan int)
go foo(c)
for v := range c {
fmt.Println(v)
}
fmt.Printf("Exiting...")
}
// send
func foo(c chan<- int) {
for i := 0; i < 10; i++ {
c <- i
}
// close is very important
// after sending all values in channel from producer
close(c)
}
Channel Select
In the given code, closing channels is not strictly required because of how the program’s logic is structured. Here’s a detailed explanation of why this works:
Why Close is Typically Used in Go
- Signaling Completion:
- Closing a channel signals that no more values will be sent on it.
- Receivers can detect this using the
v, ok := <-channel
pattern, whereok
isfalse
if the channel is closed.
- Avoiding Deadlocks:
- Without closing a channel, goroutines waiting to read from it might block indefinitely if no more values are sent.
- Iterating Over Channels:
- To range over a channel (
for v := range ch
), it must be closed to terminate the loop gracefully.
- To range over a channel (
Why It Works Without Closing in This Code
- Quit Channel Handles Termination:
- The
quit
channel is used explicitly to signal when the program should stop receiving values.When a value is sent toquit
, thereceive
function detects this in theselect
block, prints the value, and exits usingreturn
- The
case v := <-quit:
fmt.Printf("returning from loop = %d\n", v)
return
- This mechanism ensures that the program terminates the
receive
loop without relying on closed channels.
- This mechanism ensures that the program terminates the
- No Infinite Blocking:
- The
send
function sends exactly 10 values (5 odd and 5 even) and then signals termination with a single value sent toquit
. - Since
quit
is sent and immediately received, there are no goroutines left blocked indefinitely. This avoids deadlocks.
- The
- Channels Are Not Iterated:
- The code does not use
for v := range ch
to iterate over channels, which would require the channels to be closed. - Instead, values are read explicitly with
<-odd
,<-even
, and<-quit
, which do not require the channels to be closed.
- The code does not use
- Garbage Collection:
- Since the program terminates naturally after completing the
receive
loop, the channels are cleaned up by Go’s garbage collector.
- Since the program terminates naturally after completing the
When Would Closing Be Required?
- When Using
range
:- If you use
for v := range ch
, the channel must be closed to end the loop.
- If you use
- When Signaling Across Multiple Goroutines:
- If many goroutines are reading from a channel, closing the channel is a clear and efficient way to signal all of them that no more data is coming.
- Preventing Resource Leaks in Long-Running Programs:
- In long-running applications, leaving channels open could cause memory leaks if they are not garbage-collected.
Conclusion
In this program, closing the channels (odd
, even
, and quit
) is unnecessary because:
- The
quit
channel explicitly signals when to stop receiving. - There are no infinite loops or unbounded data streams that would lead to deadlocks or resource leaks.
- The program exits naturally after processing all the data.
However, best practices suggest closing channels when their usage has ended, especially in complex or long-running programs, to improve clarity and robustness.
package main
import "fmt"
func main() {
odd := make(chan int)
even := make(chan int)
quit := make(chan int)
go send(odd, even, quit)
receive(odd, even, quit)
fmt.Printf("Exiting...")
}
func send(odd, even, quit chan int) {
for i := 1; i <= 10; i++ {
if i%2 == 0 {
even <- i
} else {
odd <- i
}
}
// both works fine
quit <- 1
// quit <- 0
}
// send
func receive(odd, even, quit chan int) {
for {
select {
case v := <-odd:
fmt.Printf("odd = %d\n", v)
case v := <-even:
fmt.Printf("even = %d\n", v)
case v := <-quit:
fmt.Printf("returning from loop = %d\n", v)
return
}
}
}
Output
odd = 1
even = 2
odd = 3
even = 4
odd = 5
even = 6
odd = 7
even = 8
odd = 9
even = 10
returning from loop = 1
Exiting...
Program exited.
Fan In
package main
import (
"fmt"
"sync"
)
func main() {
even := make(chan int)
odd := make(chan int)
fanin := make(chan int)
go send(even, odd)
go receive(even, odd, fanin)
for v := range fanin {
fmt.Printf("fanin=%d\n", v)
}
fmt.Println("about to exit")
}
// send channel
func send(even, odd chan<- int) {
for i := 0; i < 10; i++ {
if i%2 == 0 {
even <- i
} else {
odd <- i
}
}
close(even)
close(odd)
}
// receive channel
func receive(even, odd <-chan int, fanin chan<- int) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
for v := range even {
fanin <- v
}
wg.Done()
}()
go func() {
for v := range odd {
fanin <- v
}
wg.Done()
}()
wg.Wait()
close(fanin)
}
Output
fanin=0
fanin=2
fanin=1
fanin=3
fanin=5
fanin=4
fanin=6
fanin=8
fanin=7
fanin=9
about to exit
Program exited.
Fan Out
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func main() {
c1 := make(chan int)
c2 := make(chan int)
go populate(c1)
go fanOutIn(c1, c2)
for v := range c2 {
fmt.Println(v)
}
fmt.Println("about to exit")
}
func populate(c chan int) {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}
func fanOutIn(c1, c2 chan int) {
var wg sync.WaitGroup
for v := range c1 {
wg.Add(1)
go func(v2 int) {
c2 <- timeConsumingWork(v2)
wg.Done()
}(v)
}
wg.Wait()
close(c2)
}
func timeConsumingWork(n int) int {
time.Sleep(time.Microsecond * time.Duration(rand.Intn(500)))
return n + rand.Intn(1000)
}
Output
185
498
382
906
469
71
168
108
333
30
about to exit
Program exited.
Please visit https: https://codeandalgo.com for more such contents.
Leave a Reply