Search code examples
goconcurrencychannelgoroutine

Go using range to loop over channel, why does it terminate before receiving all the values?


This code is a modified version of a program from section 8.4 of "The Go Programming Language" book.

package main

import (
"fmt"
)

func main() {
naturals := make(chan int)
squares := make(chan int)
done := make(chan struct{})

    // counter
    go func() {
        for x := 0; x <= 10; x++ {
            naturals <- x
        }
        done <- struct{}{}
    }()
    
    // squarer
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()
    
    // printer
    go func() {
        for x := range squares {
            fmt.Println(x)
        }
    }()
    
    <-done
    naturals <- 90
    
    close(naturals)

}

When I run this I get the output:

0 1 4 9 16 25 36 49 64 81 100

But when I change the last few lines to this:

naturals <- 90
<-done

I get the output:

8100 0 1 4 9 16 25 36 49 64 81

In the first case, when <-done is before naturals <- 90, I expected it to print all the squares of numbers 0 to 10, then print the square of 90, then close the channel and end the program.

In the second case, when naturals <- 90 is before <-done, I expected 8100 to be printed somewhere before the squares of 0 to 10, but I also expected all of them to be printed.

Can someone help me get my head around what's happening?


Solution

  • So to break it down, the first question is why you're seeing 8100 printed first. That's quite simple: the routine that pushed values 0-10 to the naturals channel is not started yet by the time you reach naturals <- 90. The channels are not buffered, so that routine waits until the value 90 is read by the routine that squares the values, and only then will you be writing the values 0-10 to the channel. This isn't guaranteed to happen all the time, but starting a routine means the runtime (in particular the scheduler) has to do some work. This work takes a bit of time, during which the main routine starts another 2 routines, and writes to the channel. That's why 90 is the first value to go through the channel, rather than 0.


    Next, you're noticing not all your values are printed. If you want all values to be printed, you should close, or write to the done channel when your routine printing the output has completed. instead of having done <- struct{}{} in the routine that writes to naturals, you should move that to the routine where you read from squares. To signal that the naturals and squares channels are "done", you can simply close them:

    go func() {
        for x := 0; x <= 10; x++ {
            naturals <- x
        }
        close(naturals) // we'll get to this in a bit
    }()
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()
    go func() {
        for x := range squares {
            fmt.Println(x)
        }
        done <- struct{}{}
    }()
    <-done // now we know everything has been printed
    close(done) // always best to close all channels explicitly
    

    Now this works like a treat, but we have a problem. There's no reliable, safe way to write our 90 value to the naturals channel except for moving that to the routine where we write values 0-10. You could get around that problem by re-using the done channel:

    go func() {
        for x := 0; x <= 10; x++ {
            naturals <- x
        }
        done <- struct{}{} // signal this routine is done
    }()
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()
    go func() {
        for x := range squares {
            fmt.Println(x)
        }
        close(done) // done channel is closed
    }()
    <-done // now we know everything has been printed
    naturals <- 90 // now we can write to this channel
    close(naturals) // signal to the square routine that was the last value
    <-done // wait for the channel to close
    

    This works, and will print the values in the order you want/expect, with 8100 as the last value, but it's a bit of a mess. Relying on a channel like done here to mean different things is a bit of a pain. We're using it to signal both that we've written all our values to the naturals channel, and then the second time to indicate that we've printed all values. That's smelly. Thankfully, we have other means of knowing when one or more routines have finished: sync.WaitGroup:

    wg := &sync.WaitGroup{}
    wg.Add(1)
    go func() {
        defer wg.Done()
        for x := 0; x <= 10; x++ {
            naturals <- x
        }
    }()
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()
    go func() {
        for x := range squares {
            fmt.Println(x)
        }
        close(done) // just close it
    }()
    wg.Wait() // wait for the first routine to return
    naturals <- 90 // now we can write to naturals
    close(naturals) // we're done, let the squares routine return, and close its channel
    // this in turn will trigger the printer routine to return
    <- done // wait for everything to be printed
    

    There's a lot more that can be said about this particular use of channels, who "owns" them, and what routine is responsible for closing which channel, why using buffered channels here might be a good idea (depending on what you're actually wanting to do/accomplish), etc... for now, though, this should help you get going and hopefully shed some light on what's going on.