题目

Redhat的首席工程师、Prometheus开源我的项目Maintainer Bartłomiej Płotka 在Twitter上出了一道Go编程题,后果超过80%的人都答复错了。

题目如下所示,答复上面这段程序的输入后果。

// named_return.gopackage mainimport "fmt"func aaa() (done func(), err error) {    return func() { print("aaa: done") }, nil}func bbb() (done func(), _ error) {    done, err := aaa()    return func() { print("bbb: surprise!"); done() }, err}func main() {    done, _ := bbb()    done()}
  • A: bbb: surprise!
  • B: bbb: surprise!aaa: done
  • C: 编译报错
  • D: 递归栈溢出

大家能够先思考下这段代码的输入后果是什么。

解析

在函数bbb最初执行return语句,会对返回值变量done进行赋值,

done := func() { print("bbb: surprise!"); done() }

留神:闭包func() { print("bbb: surprise!"); done() }里的done并不会被替换成done, err := aaa()里的done的值。

因而函数bbb执行完之后,返回值之一的done实际上成为了一个递归函数,先是打印"bbb: surprise!",而后再调用本人,这样就会陷入有限递归,直到栈溢出。因而本题的答案是D

那为什么函数bbb最初return的闭包func() { print("bbb: surprise!"); done() }里的done并不会被替换成done, err := aaa()里的done的值呢?如果替换了,那本题的答案就是B了。

这个时候就要搬出一句老话了:

This is a feature, not a bug

咱们能够看上面这个更为简略的例子,来帮忙咱们了解:

// named_return1.gopackage mainimport "fmt"func test() (done func()) {    return func() { fmt.Println("test"); done() }}func main() {    done := test()    // 上面的函数调用会进入死循环,一直打印test    done()}

正如下面代码里的正文阐明,这段程序同样会进入有限递归直到栈溢出。

如果函数test最初return的闭包func() { fmt.Println("test"); done() }里的done是被提前解析了的话,因为done是一个函数类型,done的零值是nil,那闭包里的done的值就会是nil,执行nil函数是会引发panic的。

但实际上Go设计是容许下面的代码失常执行的,因而函数test最初return的闭包里的done的值并不会提前解析,test函数执行完之后,实际上产生了上面的成果,返回的是一个递归函数,和本文开始的题目一样。

done := func() { fmt.Println("test"); done() }

因而也会进入有限递归,直到栈溢出。

总结

这个题目其实很tricky,在理论编程中,要防止对命名返回值采纳这种写法,非常容易出错。

想理解国外Go开发者对这个题目的探讨详情能够参考Go Named Return Parameters Discussion。

另外题目作者也给了如下所示的解释,原文地址能够参考具体解释:

package mainfunc aaa() (done func(), err error) {    return func() { print("aaa: done") }, nil}func bbb() (done func(), _ error) {    // NOTE(bwplotka): Here is the problem. We already defined special "return argument" variable called "done".    // By using `:=` and not `=` we define a totally new variable with the same name in    // new, local function scope.    done, err := aaa()    // NOTE(bwplotka): In this closure (anonymous function), we might think we use `done` from the local scope,    // but we don't! This is because Go "return" as a side effect ASSIGNS returned values to    // our special "return arguments". If they are named, this means that after return we can refer    // to those values with those names during any execution after the main body of function finishes    // (e.g in defer or closures we created).    //    // What is happening here is that no matter what we do in the local "done" variable, the special "return named"    // variable `done` will get assigned with whatever was returned. Which in bbb case is this closure with    // "bbb:surprise" print. This means that anyone who runs this closure AFTER `return` did the assignment    // will start infinite recursive execution.    //    // Note that it's a feature, not a bug. We use this often to capture    // errors (e.g https://github.com/efficientgo/tools/blob/main/core/pkg/errcapture/doc.go)    //    // Go compiler actually detects that `done` variable defined above is NOT USED. But we also have `err`    // variable which is actually used. This makes compiler to satisfy that unused variable check,    // which is wrong in this context..    return func() { print("bbb: surprise!"); done() }, err}func main() {    done, _ := bbb()    done()}

不过这个解释是有瑕疵的,次要是这句形容:

By using := and not = we define a totally new variable with the same name in
new, local function scope.

对于done, err := aaa(),返回变量done并不是一个新的变量,而是和函数bbb的返回变量done是同一个变量。

这里有一个小插曲:自己把这个瑕疵反馈给了原作者,原作者批准了我的意见,删除了这块解释


最新版的英文解释如下,原文地址能够参考修正版解释。

package mainfunc aaa() (done func()) {    return func() { print("aaa: done") }}func bbb() (done func()) {    done = aaa()    // NOTE(bwplotka): In this closure (anonymous function), we might think we use `done` value assigned to aaa(),    // but we don't! This is because Go "return" as a side effect ASSIGNS returned values to    // our special "return arguments". If they are named, this means that after return we can refer    // to those values with those names during any execution after the main body of function finishes    // (e.g in defer or closures we created).    //    // What is happening here is that no matter what we do with our "done" variable, the special "return named"    // variable `done` will get assigned with whatever was returned when the function ends.    // Which in bbb case is this closure with "bbb:surprise" print. This means that anyone who runs    // this closure AFTER `return` did the assignment, will start infinite recursive execution.    //    // Note that it's a feature, not a bug. We use this often to capture    // errors (e.g https://github.com/efficientgo/tools/blob/main/core/pkg/errcapture/doc.go)    return func() { print("bbb: surprise!"); done() }}func main() {    done := bbb()    done()}

思考题

上面这段代码同样应用了命名返回值,大家能够看看这个道题的输入后果是什么。能够发送音讯nrv获取答案。

package mainfunc bar() (r int) {    defer func() {        r += 4        if recover() != nil {            r += 8        }    }()        var f func()    defer f()    f = func() {        r += 2    }    return 1}func main() {    println(bar())}

开源地址

文章和示例代码开源在GitHub: Go语言高级、中级和高级教程。

公众号:coding进阶。关注公众号能够获取最新Go面试题和技术栈。

集体网站:Jincheng's Blog。

知乎:无忌。

References

  • https://twitter.com/bwplotka/...
  • https://go.dev/play/p/ELPEi2A...
  • https://go.dev/play/p/9J5a3Zt...