最近在看 Go 轻松学,请教一个在闭包函数示例部分感到困惑的问题

2014-10-29 16:22:26 +08:00
 snachx
示例 - Go闭包函数部分有段代码

https://gist.github.com/snachx/ba214c6c6629e5f4c38a#file-gistfile1-go

请问17行获得的f,到底是什么呢?我知道是一个类型为func() int的值,但是这是个什么值呢?

这段代码的输出结果:

0x401200 = 3
0x401200 = 3
0x401200 = 3

为什么三个f全是一样的呢?
2960 次点击
所在节点    问与答
16 条回复
Daizong
2014-10-29 16:43:09 +08:00
package main

import "fmt"

func main() {

var fs []func() int

for i := 0; i < 3; i++ {
fn := func(j int) func() int {
return func() int {
return j
}
}(i)
fs = append(fs, fn)
}

for _, f := range fs {
fmt.Printf("%p = %v\n", f, f())
}
}
defia
2014-10-29 16:48:47 +08:00
后面range得到的f,是一个值,其类型为函数,同一个函数所以为同一个指针

f()即函数返回值

至于为什么3个返回值都是3,我们来分析一下函数本身:
func() int {

return i
}
这个函数为一个返回int的函数
返回的数值为i

在这里,i是闭包,记住一点,通过闭包获取的函数作用域之外(这里为函数内部引用外部)的外部变量,类似于指针,即在闭包内部作用域引用外部变量时,并非像传参一样复制外部的变量数值,而是保留了外部变量的引用,所以在内部使用外部变量时,该数值取决于执行时该外部变量的值

在执行f()的时候,上面的for循环已经结束,i的数值为3,所以最终3个结果都是3

同样的再来个例子:
func main() {
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v) //通过闭包传递了v的引用
}()
}
var input string
fmt.Scanln(&input)
fmt.Println("done")
}
该例子中,最终结果输出是3个"c",而非abc
因为在runtime.GOMAXPROCS()==1的情况下,3个goroutine的执行发生于阻塞时,即fmt.Scanln时,在这个时候循环已经结束,v=="c"

这点类似javascript

for(var i=0;i<5;i++){
var j=1;
setTimeout(function(){
console.log(i+' '+j)
},200);
j=2;
}
defia
2014-10-29 16:59:11 +08:00
1楼的代码在每次循环体时,执行了一个函数,其把循环变量i作为参数传递了进去并返回一个跟原文一样功能的函数,这样一来j就是复制传参了,最终就会返回123
snachx
2014-10-29 17:22:20 +08:00
@defia

其他的我都能理解也是这么想的,就是第一句不明白,为什么是同一个函数呢?

如果生成fs的那段我写成这样:


for i := 0; i < 3; i++ {
if i == 0 {
a := func() int {
return i
}
fs = append(fs, a)
}
if i == 1 {
a := func() int {
return i
}
fs = append(fs, a)
}
if i == 2 {
a := func() int {
return i
}
fs = append(fs, a)
}
}

结果就会是:
0x401410 3
0x401420 3
0x401430 3

这样写为什么就不是同一个函数了呢?

我认为func() int { return i}就是函数定义了啊,循环三次就相当于三次定义了...
oaix
2014-10-29 17:28:34 +08:00
我觉得是被编译器优化掉了。
defia
2014-10-29 17:35:55 +08:00
@snachx 我也认为这个取决于编译器,而且指针是否一样在golang中意义并不大
snachx
2014-10-29 17:44:09 +08:00
@defia
@oaix

其实这个确实也不会对实际使用造成影响,只是没能理解为什么行为有差异,如果是因为编译器,那我就不纠结了…
gihnius
2014-10-29 18:31:55 +08:00
@snachx 当然不是因为编译器,闭包就是这样!你要看 f() *运行时的上下文* ,i 就是等于 3 嘛。
gihnius
2014-10-29 18:39:08 +08:00
@snachx 你可以这样试试:


for i := 0; i < 3; i++ {
func(n int){
fs = append(fs, func() int {
return n
})
}(i)
}
chemzqm
2014-10-29 19:25:52 +08:00
楼上说法是有问题的,遍历的时候肯定是了3个函数,虽然它们代码相同,但是range的时候go并没有重新声明变量,所以f使用的地址不会变的,但它指向的函数变了,3个函数返回的是同一个变量i。你试试append 3个不同的函数就知道了。
snachx
2014-10-29 19:55:58 +08:00
@gihnius

好像不是你说的这个问题,i等于3是没有异议的,我主要是问为什么range之后三个f值是一样的,不是问i的值
snachx
2014-10-29 19:58:52 +08:00
@chemzqm

你说的好像也有问题,好像也不是,比如我在4楼贴的代码,f打出来就是不同的,range是没有重新声明,但是每次赋值了啊,打印f是打印f的值而不是打印f的地址
defia
2014-10-29 22:17:08 +08:00
@snachx 编译器优化啦

package main

import "fmt"

func main() {
for i := 0; i < 3; i++ {
a := []string{"str"}
fmt.Printf("%p\n", a)
}
}
这样就是3个不同值

package main

import "fmt"

func main() {
for i := 0; i < 3; i++ {
a := func() {}
fmt.Printf("%p\n", a)
}
}

这样就是三个相同值..
可见编译器会对内联函数有优化.当然也很好理解,不然每个函数一套还调试个屁啊
defia
2014-10-29 22:18:29 +08:00
所以其实内联函数/匿名函数,本身跟普通的命名函数是完全一回事的
gihnius
2014-10-29 22:22:36 +08:00
@snachx 哦,我以为是值的问题。不过那函数地址没啥可以大惊小怪的吧!
snachx
2014-10-29 22:27:03 +08:00
@gihnius 是的啊,完全是我自己纠结了一下而已,因为我老觉得那是三次函数定义就该是三个函数。。。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/142406

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX