为什么在 defer 语句中关闭通道会出现恐慌?

在下面的示例中,go 例程将值泵入无缓冲通道,并且 main 函数对其进行迭代。


package main


import (

    "fmt"

    "strconv"

)


var chanStr chan string


func main() {

    go pump()

    fmt.Println("iterating ...")

    for val := range chanStr {

        fmt.Printf("fetched val: %s from channel\n", val)

    }

}


func pump() {

    defer close(chanStr)

    chanStr = make(chan string)

    for i := 1; i <= 5; i++ {

        fmt.Printf("pumping seq %d into channel\n", i)

        chanStr <- "val" + strconv.Itoa(i)

    }

    //close(chanStr)

}

该函数会出现恐慌并输出以下内容:


iterating ...                                             

pumping seq 1 into channel                                

pumping seq 2 into channel                                

fetched val: val1 from channel                            


......


fetched val: val4 from channel                            

pumping seq 5 into channel                                

panic: close of nil channel                               


goroutine 5 [running]:                                    

main.pump()                                               

        C:/personal/gospace/go-rules/test.go:26 +0x1a6    

created by main.main                                      

        C:/personal/gospace/go-rules/test.go:11 +0x4e     

但是,如果我评论 defer 语句并在 goroutine 中的 for 循环之后立即关闭pump,接收器不会惊慌。 这两种情况有什么区别?看起来 defer 在收到值之前关闭通道,但常规关闭会等待。


此外,当我使用竞争检测器进行构建时,即使在常规关闭中,它也会标记潜在的竞争条件(我无法每次都重新创建竞争)。这是否意味着这两种方式都不能优雅地关闭通道?


更新: 对于所有评论,我知道问题是什么。我必须在函数的第一行创建通道main()。不过,我在 Windows 上运行 go1.12,并且观察到了这种行为。显然我没有伪造输出。我一直使用 defer 语句重新创建恐慌,甚至在 for 循环后立即关闭通道时也没有发生过恐慌pump()


HUH函数
浏览 205回答 3
3回答

梵蒂冈之花

您的代码在不同方面都非常活泼:for val在 goroutine 实际初始化通道之前,您有可能(事实上,很有可能)开始从循环中的通道读取数据,从而导致死锁。iterating ...pumping seq 1 into channelfatal error: all goroutines are asleep - deadlock!事实上,这是我观察到的在本地或操场上按原样执行代码的唯一行为。如果我添加一个延迟,&nbsp;fmt.Println("iterating ...")&nbsp;time.Sleep(10 * time.Millisecond) // Delay ensures the channel has been created&nbsp;for val := range chanStr {然后我确实观察了您注意到的行为:iterating ...pumping seq 1 into channelfetched val: val1 from channelpumping seq 2 into channelpumping seq 3 into channelfetched val: val2 from channelfetched val: val3 from channelpumping seq 4 into channelpumping seq 5 into channelfetched val: val4 from channelfetched val: val5 from channelpanic: close of nil channel原因是你在调用close(chanStr)时chanStr仍然为零。defer如果您在创建频道后致电您:func pump() {&nbsp; &nbsp; chanStr = make(chan string)&nbsp; &nbsp; defer close(chanStr)你会解决这个问题的。要解决这两个竞争,您需要在调用 goroutine之前初始化通道。完整代码:package mainimport (&nbsp; &nbsp; "fmt"&nbsp; &nbsp; "strconv")var chanStr chan stringfunc main() {&nbsp; &nbsp; chanStr = make(chan string)&nbsp; &nbsp; go pump(chanStr)&nbsp; &nbsp; fmt.Println("iterating ...")&nbsp; &nbsp; for val := range chanStr {&nbsp; &nbsp; &nbsp; &nbsp; fmt.Printf("fetched val: %s from channel\n", val)&nbsp; &nbsp; }}func pump(chanStr chan string) {&nbsp; &nbsp; defer close(chanStr)&nbsp; &nbsp; for i := 1; i <= 5; i++ {&nbsp; &nbsp; &nbsp; &nbsp; fmt.Printf("pumping seq %d into channel\n", i)&nbsp; &nbsp; &nbsp; &nbsp; chanStr <- "val" + strconv.Itoa(i)&nbsp; &nbsp; }}为了进一步说明问题是立即defer close(chanStr)评估chanStr(当它仍然是nil)时,请考虑这个(不推荐!)替代解决方案:package mainimport (&nbsp; &nbsp; "fmt"&nbsp; &nbsp; "strconv"&nbsp; &nbsp; "time")var chanStr chan stringfunc main() {&nbsp; &nbsp; go pump()&nbsp; &nbsp; fmt.Println("iterating ...")&nbsp; &nbsp; time.Sleep(10 * time.Millisecond)&nbsp; &nbsp; for val := range chanStr {&nbsp; &nbsp; &nbsp; &nbsp; fmt.Printf("fetched val: %s from channel\n", val)&nbsp; &nbsp; }}func pump() {&nbsp; &nbsp; defer func() {&nbsp; &nbsp; &nbsp; &nbsp; close(chanStr)&nbsp; &nbsp; }()&nbsp; &nbsp; chanStr = make(chan string)&nbsp; &nbsp; for i := 1; i <= 5; i++ {&nbsp; &nbsp; &nbsp; &nbsp; fmt.Printf("pumping seq %d into channel\n", i)&nbsp; &nbsp; &nbsp; &nbsp; chanStr <- "val" + strconv.Itoa(i)&nbsp; &nbsp; }}在这种情况下,延迟函数是 的闭包chanStr,因此chanStr的计算被延迟到实际执行。在此版本中,当延迟函数执行时,chanStr不再为 nil,因此无需恐慌。

手掌心

主 go 例程有可能在创建通道之前从通道中读取数据。这就是你的数据竞赛。应在开始您的 go 例程之前创建通道。修复: https:&nbsp;//play.golang.org/p/O7pgM05KEtI

慕标5832272

您发布的代码存在死锁情况。您可能没有发布相同的代码。这是应该可以工作的更新后的代码:package mainimport (    "fmt"    "strconv")func main() {    chanStr := make(chan string)    go pump(chanStr)    fmt.Println("iterating ...")    for val := range chanStr {        fmt.Printf("fetched val: %s from channel\n", val)    }}func pump(ch chan string) {    defer close(ch)    for i := 1; i <= 5; i++ {        fmt.Printf("pumping seq %d into channel\n", i)        ch <- "val" + strconv.Itoa(i)    }    //close(chanStr)}本例中的问题是,您在函数内部创建了通道pump,因此主函数不知道如何使用数据,并且由于没有使用者而导致死锁。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go