Go ticker 示例不是 select 'done' 的情况吗?

Go ticker example doesn't select the 'done' case?

我已将此示例 https://gobyexample.com/tickers 改编为以下脚本:

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(500 * time.Millisecond)
    done := make(chan bool)

    go func() {
        for {
            select {
            case <-done:
                fmt.Println("Received 'done'")
                return
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
            }
        }
    }()

    time.Sleep(1600 * time.Millisecond)
    // ticker.Stop()
    done <- true
    // fmt.Println("Ticker stopped.")
}

与引用示例的两个区别是我注释掉了 ticker.Stop() 行并在 case <-done 块中添加了 fmt.Println("Received 'done'") 行。如果我 运行 这个,我观察到以下输出:

> go run tickers.go
Tick at 2019-10-06 15:25:50.576798 -0700 PDT m=+0.504913907
Tick at 2019-10-06 15:25:51.074993 -0700 PDT m=+1.003102855
Tick at 2019-10-06 15:25:51.576418 -0700 PDT m=+1.504521538

我的问题:为什么它不打印 Received 'done' 到终端?

奇怪的是,如果我在 Ticker stopped Println 语句中发表评论,我也会看到 Received 'done'

> go run tickers.go
Tick at 2019-10-06 15:27:30.735163 -0700 PDT m=+0.504666656
Tick at 2019-10-06 15:27:31.234076 -0700 PDT m=+1.003573649
Tick at 2019-10-06 15:27:31.735342 -0700 PDT m=+1.504833296
Ticker stopped.
Received 'done'

我记得,Goroutine 中的代码可以同步假设为 运行,所以我很困惑,我没有看到前者中 Println 语句的效果因为它发生在 Goroutine returns 之前。有人可以解释一下吗?

发生done <- true时,main函数直接返回

您可以添加另一个 time.Sleep() 以查看发生了什么。

    time.Sleep(1600 * time.Millisecond)
    // ticker.Stop()
    done <- true
    // fmt.Println("Ticker stopped.")
    time.Sleep(1600 * time.Millisecond)

... why does it not print Received 'done' to the terminal?

确实如此——或者更确切地说,它尝试

Go 程序在主 goroutine(调用包 mainmain)returns(或在此处未发生的各种情况下更快)时退出。您的 main returns 在调用 time.Sleep() 然后在 done 上发送 true 之后。

同时,for 循环中的 goroutine 在 true 值到达 done 通道时唤醒。这发生在 main goroutine 发送它之后,之后 main goroutine 正在退出。

如果在这个退出过程中,主goroutine耗时足够长,匿名goroutine就有时间打印Received 'done'。如果在这个退出过程中,主 goroutine 足够快,匿名 goroutine 永远不会完成,或者甚至永远不会开始,打印任何东西,你什么也看不到。 (实际输出由单个底层系统调用完成,因此您要么获取所有输出,要么获取其中的 none。)

您可以通过多种机制确保您分离的 goroutine 在让您的主要 goroutine 退出之前完成,但最简单的可能是使用 sync.WaitGroup,因为它是为此而设计的。创建一个等待组,将其计数器设置为 1(将其初始零加 1),然后在退出匿名 goroutine 的途中调用 Done 函数。让主 goroutine 等待它。

参见 Go Playground example