go tutorial select statement

2020-08-19 02:12发布

问题:

I'm working through the examples at tour.golang.org, and I've encountered this code I don't really understand:

package main
import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x: // case: send x to channel c?
            x, y = y, x+y
        case <-quit: // case: receive from channel quit?
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() { // when does this get called?
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

I understand the basics of how channels work, but what I don't get is how the above select statement works. The explanation on the tutorial says:

"The select statement lets a goroutine wait on multiple communication operations. A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready."

But how are the cases getting executed? From what I can tell, they're saying:

case: send x to channel c

case: receive from quit

I think I understand that the second one executes only if quit has a value, which is done later inside the go func(). But what is the first case checking for? Also, inside the go func(), we're apparently printing values from c, but c shouldn't have anything in it at that point? The only explanation I can think of is that the go func() somehow executes after the call to fibonacci(). I'm guessing it's a goroutine which I don't fully understand either, it just seems like magic.

I'd appreciate if someone could go through this code and tell me what it's doing.

回答1:

Remember that channels will block, so the select statement reads:

select {
case c <- x: // if I can send to c
    // update my variables
    x, y = y, x+y
case <-quit: // If I can receive from quit then I'm supposed to exit
    fmt.Println("quit")
    return
}

The absence of a default case means "If I can't send to c and I can't read from quit, block until I can."

Then in your main process you spin off another function that reads from c to print the results

for i:=0; i<10; i++ {
    fmt.Println(<-c)  // read in from c
}
quit <- 0  // send to quit to kill the main process.

The key here is to remember that channels block, and you're using two unbuffered channels. Using go to spin off the second function lets you consume from c so fibonacci will continue.


Goroutines are so-called "green threads." Starting a function call with the keyword go spins it off into a new process that runs independent of the main line of execution. In essence, main() and go func() ... are running simultaneously! This is important since we're using a producer/consumer pattern in this code.

fibonacci produces values and sends them to c, and the anonymous goroutine that's spawned from main consumes values from c and processes them (in this case, "processing them" just means printing to the screen). We can't simply produce all the values and then consume them, because c will block. Furthermore fibonacci will produce more values forever (or until integer overflow anyway) so even if you had a magic channel that had an infinitely long buffer, it would never get to the consumer.



回答2:

There are two key things to understanding this code example:

First, let's review how unbuffered channels work. From the documentation

If the channel is unbuffered, the sender blocks until the receiver has received the value.

Note that both channels in the code example, c and quit are unbuffered.

Secondly, when we use the go keyword to start a new goroutine, the execution will happen in parallel with other routines. So in the example, we have two go routines running: the routine started by func main(), and the routine started by go func()... inside the func main().

I added some inline comments here which should make things clearer: package main import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for { // this is equivalent to a while loop, without a stop condition
        select {
        case c <- x: // when we can send to channel c, and because c is unbuffered, we can only send to channel c when someone tries to receive from it
            x, y = y, x+y
        case <-quit: // when we can receive from channel quit, and because quit is unbuffered, we can only receive from channel quit when someone tries to send to it
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() { // this runs in another goroutine, separate from the main goroutine
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit) // this doesn't start with the go keyword, so it will run on the go routine started by func main()
}


回答3:

You've pretty much got it.

inside the go func(), we're apparently printing values from c, but c shouldn't have anything in it at that point? The only explanation I can think of is that the go func() somehow executes after the call to fibonacci(). I'm guessing it's a goroutine

Yes, the go keyword starts a goroutine, so the func() will run at the same time as the fibonacci(c, quit). The receive from the channel in the Println simply blocks until there is something to receive