This blog will go over how to use range
to read data from a channel and close
to shut it down.
To run the different cases/examples, please use the code on GitHub
You can use <-
(e.g. <-myChannel
) to accept values from a channel. Here is an example:
Simple scenario
func f1() {
c := make(chan int)
//producer
go func() {
for i := 1; i <= 5; i++ {
c <- i
time.Sleep(1 * time.Second)
}
}()
//consumer
go func() {
for x := 1; x <= 5; x++ {
i := <-c
fmt.Println("i =", i)
}
fmt.Println("press ctrl+c to exit")
}()
e := make(chan os.Signal)
signal.Notify(e, syscall.SIGINT, syscall.SIGTERM)
<-e
}
The producer goroutine sends five integers and the consumer goroutine accepts them. The fact that the number of records exchanged (five in this example) is fixed/known makes means that this is an ideal scenario. The consumer knows exactly when to finish/exit
If you run this: the receiver will print 1 to 5, asking you to exit
To run, simply uncomment
f1()
inmain
and run the program usinggo run channels-range-close.go
i = 1
i = 2
i = 3
i = 4
i = 5
consumer finished. press ctrl+c to exit
producer finished
^C
goroutine leak
This was an oversimplified case. Let's make a small change by removing the for
loop counter and converting it into an infinite
loop - this is to simulate a scenario where the receiver wants to get all the values sent by the producer but does not know the specifics i.e. how many values will be sent (in real applications, this is often the case)
//consumer
go func() {
for {
i := <-c
fmt.Println("i =", i)
}
fmt.Println("consumer finished. press ctrl+c to exit")
}()
The output from the modified program is:
To run, simply uncomment
f2()
inmain
and run the program usinggo run channels-range-close.go
i = 1
i = 2
i = 3
i = 4
i = 5
producer finished
Notice that the producer goroutine exited but you did not see the consumer finished. press ctrl+c to exit
message. Once the producer is done sending five integers and the consumer receives all of them, it's just stuck there waiting for next value i := <-c
and will not be able to return/exit
In long running programs, this results in a
goroutine leak
range
and close
to the resuce!
This is where range
and close
can help:
-
range
provides a way to iterate over values of a channel (just like you would for a slice) -
close
makes it possible to signal to consumers of a channel that there is nothing else which will be sent on this channel
Let's refactor the program. First, change the consumer to use range
- remove the i := <-c
bit and replace it with for i := range c
go func() {
for i := range c {
fmt.Println("i =", i)
}
fmt.Println("consumer finished. press ctrl+c to exit")
}()
Update the producer goroutine to add close(c)
outside the for
loop. This will ensure that the consumer goroutine gets the signal that there is nothing more to come from the channel and the range
loop will terminate!
go func() {
for i := 1; i <= 5; i++ {
c <- i
time.Sleep(1 * time.Second)
}
close(c)
fmt.Println("producer finished")
}()
If you run the program now, you should see this output:
To run, simply uncomment
f3()
inmain
and run the program usinggo run channels-range-close.go
i = 1
i = 2
i = 3
i = 4
i = 5
producer finished
consumer finished. press ctrl+c to exit
bonus
The consumer goroutine does not have to co-exist with the producer goroutine to receive the values i.e. even if the producer goroutine finishes (and closes the channel), the consumer goroutine range
loop will receive all the values - this is helpful when the consumer is processing the records sequentially.
We can simulate this scenario by using a combination of:
- a buffered channel in the producer, and
- delay the consumer goroutine by adding a
time.Sleep()
In the producer, we create a buffered channel of capacity five c := make(chan int, 5)
. This is to ensure that producer goroutine will not block in the absence of a consumer:
c := make(chan int, 5)
//producer
go func() {
for i := 1; i <= 5; i++ {
c <- i
}
close(c)
fmt.Println("producer finished")
}()
The consumer remains the same, except for the time.Sleep(5 * time.Second)
which allows the producer goroutine to exit before consumer can start off
go func() {
time.Sleep(5 * time.Second)
fmt.Println("consumer started")
for i := range c {
fmt.Println("i =", i)
}
fmt.Println("consumer finished. press ctrl+c to exit")
}()
Here is the output you should see:
To run, simply uncomment
f4()
inmain
and run the program usinggo run channels-range-close.go
producer finished
consumer started
i = 1
i = 2
i = 3
i = 4
i = 5
consumer finished. press ctrl+c to exit
The producer goroutine finished sending five records, the consumer woke up after a while, received and printed out all the five messages sent by the producer.
That's all for now. Stay tuned for upcoming posts in the series!
Top comments (0)