对命名返回值的修改操作在另外一个协程中执行时,为什么输出返回值时会得到在那个协程中被修改后的值?

最近看到一段代码,大致如下:

package main

import (
    "fmt"
    "time"
)

var ch = make(chan func())

func doIt() (res int) {
    ch <- func() {
        res = 3
    }
    return
}

func main() {
    go func() {
        for {
            time.Sleep(1 * time.Second)
            (<-ch)()
        }
    }()
    fmt.Println(doIt())
}

运行之后,控制台的输出结果如下:

(base) rex@MacBook-Pro func % go run named_return_val.go
3
(base) rex@MacBook-Pro func %

其表现出来的结果为:程序阻塞了一秒钟,然后输出数字3

然而,根据命名返回值方法的定义,定义了命名返回值时,相当于在函数体内第一行声明了该变量,此时该变量具有默认的零值

另外,doIt函数内部写入channel ch的匿名函数在另外一个协程中在一秒钟之后执行,主协程并没有等待其他协程,为什么最后输出结果是3而不是0


我的go版本:

(base) rex@MacBook-Pro func % go version
go version go1.15 darwin/amd64
pardon110
最佳答案

time.Sleep关系不大,关键是全局无缓冲匿名函数通道 ch读写间接导致groutine 上下文切换 。
主协程并非没有等待其它协程,调用doIt函数,ch写操作时阻塞了,这样写你可能会明白

package main

import (
    "fmt"
    "log"
)

var ch = make(chan func())

func doIt() int {
    log.Print("enter doIt...")
    res := 0
    ch <- func() { // 2. ch 通道写入 第一次阻塞切换
        res = 3
        log.Print("write ch...")
    }
    //4. 通道ch读后,匿名函数执行,res被赋值为3
    log.Print("return res...")
    return res // 5.返回
}

func main() {
    log.Print("enter main...")
    go func() {
        log.Print("enter goroutine...")
        for {
            log.Print("read ch....")
            (<-ch)() // 3.通道ch第一次读同步无阻塞,死循环第二次读时阻塞切换主main即继续执行doit余下部分
            log.Print("after read ch...")
        }
    }()

    fmt.Println(doIt()) // 1. 同步执行 doIt
    log.Print("exit main...") //6.主goroutine (main)直接退出,ch读阻塞无用武之地
}

输出

$ go run "d:\code-base\gomod\tt\web\test\tmp.go"
2021/03/22 20:34:04 enter main...
2021/03/22 20:34:04 enter doIt...
2021/03/22 20:34:04 enter goroutine...
2021/03/22 20:34:04 read ch....
2021/03/22 20:34:04 write ch...
2021/03/22 20:34:04 after read ch...
2021/03/22 20:34:04 read ch....
2021/03/22 20:34:04 return res...
3
2021/03/22 20:34:04 exit main...
3年前 评论
clarifysky (楼主) 3年前
讨论数量: 2
aab

我觉得可以稍微调整一下程序看看,还需要知道闭包会对它的执行环境进行一个打包。

package main

import (
    "fmt"
    "time"
)

var ch = make(chan func())

func doIt() (res int) {
    //  会一致阻塞直到消费者启动
    ch <- func() {
        time.Sleep(1 * time.Second)
        res = 3
    }
    return
}

func main() {
    go func() {
        for {
            time.Sleep(1 * time.Second)
            (<-ch)()
        }
    }()
    fmt.Println(doIt())
}
3年前 评论
clarifysky (楼主) 3年前
pardon110

time.Sleep关系不大,关键是全局无缓冲匿名函数通道 ch读写间接导致groutine 上下文切换 。
主协程并非没有等待其它协程,调用doIt函数,ch写操作时阻塞了,这样写你可能会明白

package main

import (
    "fmt"
    "log"
)

var ch = make(chan func())

func doIt() int {
    log.Print("enter doIt...")
    res := 0
    ch <- func() { // 2. ch 通道写入 第一次阻塞切换
        res = 3
        log.Print("write ch...")
    }
    //4. 通道ch读后,匿名函数执行,res被赋值为3
    log.Print("return res...")
    return res // 5.返回
}

func main() {
    log.Print("enter main...")
    go func() {
        log.Print("enter goroutine...")
        for {
            log.Print("read ch....")
            (<-ch)() // 3.通道ch第一次读同步无阻塞,死循环第二次读时阻塞切换主main即继续执行doit余下部分
            log.Print("after read ch...")
        }
    }()

    fmt.Println(doIt()) // 1. 同步执行 doIt
    log.Print("exit main...") //6.主goroutine (main)直接退出,ch读阻塞无用武之地
}

输出

$ go run "d:\code-base\gomod\tt\web\test\tmp.go"
2021/03/22 20:34:04 enter main...
2021/03/22 20:34:04 enter doIt...
2021/03/22 20:34:04 enter goroutine...
2021/03/22 20:34:04 read ch....
2021/03/22 20:34:04 write ch...
2021/03/22 20:34:04 after read ch...
2021/03/22 20:34:04 read ch....
2021/03/22 20:34:04 return res...
3
2021/03/22 20:34:04 exit main...
3年前 评论
clarifysky (楼主) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!