为什么这个 select 总是 运行 实际执行第一个 case 时的默认 case?

Why does this select always run the default case when the first case actually is executed?

我正在尝试更好地了解 golang 频道。在阅读 this article 时,我正在研究非阻塞发送,并提出了以下代码:

package main
import (
    "fmt"
    "time"
)

func main() {
    stuff := make(chan int)
    go func(){
        for i := 0; i < 5; i ++{
            select {
            case stuff <- i:
                fmt.Printf("Sent %v\n", i)
            default:
                fmt.Printf("Default on %v\n", i)
            }
        }
        println("Closing")
        close(stuff)
    }()
    time.Sleep(time.Second)
    fmt.Println(<-stuff)
    fmt.Println(<-stuff)
    fmt.Println(<-stuff)
    fmt.Println(<-stuff)
    fmt.Println(<-stuff)
}

这将打印:

Default on 0
Default on 1
Default on 2
Default on 3
Default on 4
Closing
0
0
0
0
0

虽然我知道只有 0s 会被打印,但我真的不明白为什么第一次发送仍然触发 select 的 default 分支?

在这种情况下,select 行为背后的逻辑是什么?

Example at the Go Playground

它只执行默认情况,因为 for 循环在任何内容开始从通道读取之前运行了 5 次。每次通过时,因为无法从通道中读取任何内容,所以会转到默认情况。如果有东西可以从通道读取,它就会执行那个案例。

您永远不会向 stuff 发送任何值,您会在到达 fmt.Println 语句中的任何接收操作之前执行所有默认情况。如果没有其他操作可以继续,则 default 情况会立即执行,这意味着您的循环将尽快执行并 return。

您想阻止循环,因此不需要 default 案例。你也不需要最后的 close ,因为你不依赖于关闭的通道解锁接收或中断 range 子句。

stuff := make(chan int)
go func() {
    for i := 0; i < 5; i++ {
        select {
        case stuff <- i:
            fmt.Printf("Sent %v\n", i)
        }
    }
    println("Closing")
}()
time.Sleep(time.Second)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)

https://play.golang.org/p/k2rmRDP38f

还要注意最后的 "Sent" 和 "Closing" 行没有被打印出来,因为你没有其他同步等待 goroutine 完成,但是这不会影响结果这个例子。

您的第一个案例没有执行。

这是您的程序的作用:

  1. 启动一个 goroutine。
  2. 尝试在频道上通过4发送0,全部阻塞,因为没有任何内容读取频道,所以失败。
  3. 与此同时,在主 goroutine 中,您正在休眠一秒钟...
  4. 然后在第二秒过去后,尝试从通道读取,但它已关闭,因此每次都会得到 0

要获得您想要的行为,您有两种选择:

  1. 使用缓冲通道,它可以容纳您发送的所有数据:

    stuff := make(chan int, 5)
    
  2. 不要在你的select语句中使用default,这将导致每次发送都要等待直到它可以成功。

首选哪个取决于您的目标。对于像这样的最小示例,两者可能没有好坏之分。

由于您使用的是非阻塞 'send',因此 stuff <- i 实际上只会在 reader 已经在等待读取通道上的内容时才会执行,或者如果该频道有一些缓冲区。如果不是,'send' 将不得不阻止。

现在,由于您在从频道读取的打印语句之前有一个 time.Sleep(time.Second),因此在 1 秒过去之前,频道没有 reader。另一方面,goroutine 在该时间内完成执行并且不发送任何内容。

您在输出中看到全为零​​,因为 fmt.Println(...) 语句是从一个关闭的通道中读取的。