loading...
ITNEXT

How to manage Go channels using range and close

abhirockzz profile image Abhishek Gupta Updated on ・4 min read

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() in main and run the program using go 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() in main and run the program using go 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() in main and run the program using go 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() in main and run the program using go 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!

Posted on by:

abhirockzz profile

Abhishek Gupta

@abhirockzz

Currently working with Kafka, Databases, Azure, Kubernetes and related open source projects | Confluent Community Catalyst (for Kafka)

ITNEXT

ITNEXT is a platform for software developers, engineers, IT architects, system engineers and IT enthousiasts to share knowledge, connect and connect.

Discussion

pic
Editor guide