共计 4451 个字符,预计需要花费 12 分钟才能阅读完成。
题目
Redhat 的首席工程师、Prometheus 开源我的项目 Maintainer Bartłomiej Płotka 在 Twitter 上出了一道 Go 编程题,后果超过 80% 的人都答复错了。
题目如下所示,答复上面这段程序的输入后果。
// named_return.go
package main
import "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.go
package main
import "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 main
func 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 main
func 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 main
func 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…