V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
snachx
V2EX  ›  问与答

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

  •  1
     
  •   snachx · 2014-10-29 16:22:26 +08:00 · 2963 次点击
    这是一个创建于 3684 天前的主题,其中的信息可能已经有所发展或是发生改变。
    示例 - Go闭包函数部分有段代码

    #file-gistfile1-go

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

    这段代码的输出结果:

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

    为什么三个f全是一样的呢?
    16 条回复    2014-10-29 22:27:03 +08:00
    Daizong
        1
    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
        2
    defia  
       2014-10-29 16:48:47 +08:00   ❤️ 1
    后面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
        3
    defia  
       2014-10-29 16:59:11 +08:00
    1楼的代码在每次循环体时,执行了一个函数,其把循环变量i作为参数传递了进去并返回一个跟原文一样功能的函数,这样一来j就是复制传参了,最终就会返回123
    snachx
        4
    snachx  
    OP
       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
        5
    oaix  
       2014-10-29 17:28:34 +08:00
    我觉得是被编译器优化掉了。
    defia
        6
    defia  
       2014-10-29 17:35:55 +08:00
    @snachx 我也认为这个取决于编译器,而且指针是否一样在golang中意义并不大
    snachx
        7
    snachx  
    OP
       2014-10-29 17:44:09 +08:00 via Android
    @defia
    @oaix

    其实这个确实也不会对实际使用造成影响,只是没能理解为什么行为有差异,如果是因为编译器,那我就不纠结了…
    gihnius
        8
    gihnius  
       2014-10-29 18:31:55 +08:00
    @snachx 当然不是因为编译器,闭包就是这样!你要看 f() *运行时的上下文* ,i 就是等于 3 嘛。
    gihnius
        9
    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
        10
    chemzqm  
       2014-10-29 19:25:52 +08:00
    楼上说法是有问题的,遍历的时候肯定是了3个函数,虽然它们代码相同,但是range的时候go并没有重新声明变量,所以f使用的地址不会变的,但它指向的函数变了,3个函数返回的是同一个变量i。你试试append 3个不同的函数就知道了。
    snachx
        11
    snachx  
    OP
       2014-10-29 19:55:58 +08:00 via Android
    @gihnius

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

    你说的好像也有问题,好像也不是,比如我在4楼贴的代码,f打出来就是不同的,range是没有重新声明,但是每次赋值了啊,打印f是打印f的值而不是打印f的地址
    defia
        13
    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
        14
    defia  
       2014-10-29 22:18:29 +08:00
    所以其实内联函数/匿名函数,本身跟普通的命名函数是完全一回事的
    gihnius
        15
    gihnius  
       2014-10-29 22:22:36 +08:00
    @snachx 哦,我以为是值的问题。不过那函数地址没啥可以大惊小怪的吧!
    snachx
        16
    snachx  
    OP
       2014-10-29 22:27:03 +08:00
    @gihnius 是的啊,完全是我自己纠结了一下而已,因为我老觉得那是三次函数定义就该是三个函数。。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2942 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 11:22 · PVG 19:22 · LAX 03:22 · JFK 06:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.