V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
main1234
V2EX  ›  程序员

golang 关于 forrange 的一些疑问

  •  1
     
  •   main1234 · 11 天前 · 1323 次点击
    package main
    
    import "fmt"
    
    type Users struct {
    	Name string
    }
    
    func (u *Users) GetName() {
    	fmt.Println(u.Name)
    }
    
    func main() {
    	users1 := []Users{{"a"}, {"b"}, {"c"}}
    	for _, u := range users1 {
    		defer u.GetName() //c  c  c
    	}
    
    	users2 := []*Users{{"x"}, {"y"}, {"z"}}
    	for _, u := range users2 {
    		defer u.GetName() //z x y
    	}
    }
    
    

    我理解 forrange 的原理,无论第一个还是第二个 forrange ,u 都是同一个地址

    对于第一个 forrange ,由于 u 是同一个地址,for 执行完毕后 u 地址指向最后一个{"c"},所以输出的都是 ccc

    对于第二个 forrange ,我想不明白,for 循环完后,u 不是也是指向最后一个{"z"},那么输出的为啥不是 zzz

    求大佬赐教

    18 条回复    2024-05-13 18:22:39 +08:00
    gerorim
        1
    gerorim  
       10 天前 via iPhone
    在第一个 for 中,u 是 users1 数组中每个元素的副本。当使用 defer 时,scheduler 对 GetName()的调用在函数末尾执行(即 main())。重要的是,defer 捕获变量 u 本身,而不是 u 在每次迭代时指向的值。因为 u 是一个结构(不是指针),它会被循环的每次迭代覆盖,到 main()退出时,u 为循环的最后一个值,即{Name: "c"}。因此,GetName ()打印“c”三次。
    gerorim
        2
    gerorim  
       10 天前 via iPhone
    第二个 for ,u 是一个指针,直接指向用户 2 切片中的每个元素。同样,defer 捕获 u ,但在这里,每个 u 都是一个不同的指针,指向不同的地址。循环分别捕获每个指针,当延迟执行对 GetName()的调用时,每个指针都指向不同的用户结构。此外,由于延迟执行 LIFO ( Last In ,First Out )顺序的函数,所以应该看到“z”、“y”、“x”(循环顺序的反转),而不是楼主所要的“z”、“z”、“z”。
    twl007
        3
    twl007  
       10 天前   ❤️ 2
    Fixing For Loops in Go 1.22
    https://go.dev/blog/loopvar-preview
    twl007
        4
    twl007  
       10 天前 via iPhone   ❤️ 1
    第一个行为在 1.22 修复了

    在 go mid 里面的的版本小于 1.22 的时候会继续保持以前的行为 在版本大于 1.22 的时候会修正这个问题
    lance6716
        5
    lance6716  
       10 天前 via Android   ❤️ 1
    因为你的 GetName 定义在 *Users 上,当变量是类型 Users 会有一个隐含的取地址,再加上旧版本 for loop 用的是同一个地址,就会变成 ccc
    povsister
        6
    povsister  
       10 天前 via iPhone   ❤️ 3
    一楼说的属于是牛头不对马嘴了… 这两个 for 没有本质区别,都是在不停对局部变量 u 进行赋值,op 疑惑的这个问题其实隐含了 3 个问题:
    1. defer 的本质是什么?
    2. go 编译器的自动 takeRef/deRef
    3. func with receiver 到底是什么?

    简单来说你可以认为,defer 会把函数压入栈中,而且函数参数的 evaluate 发生在 defer 语句那一行

    所以 循环一的实际 defer 是这样的
    defer GetName(&u)
    循环二代实际 defer 是
    defer GetName(u)

    注意两个&的区别,再结合我说的,参数 evaluate 发生在 defer 语句书写时,这下 ,op 理解了否?

    留个思考题。
    defer func() { u.GetName() }
    这个输出什么呢?(笑
    lance6716
        7
    lance6716  
       10 天前 via Android
    第二个 for loop:defer 并不是闭包,所以跟你说的“指向最后一个”没关系。你是直接被第一个 for loop 搞迷糊了,误以为自己掌握了某个坑能解释这个奇怪行为,其实只是瞎猫碰上死耗子

    建议升级到 go1.22 直接避免踩坑
    main1234
        8
    main1234  
    OP
       10 天前
    @povsister 我有点懵了,对于第二个 forrange 来说,defer 相当于压栈,底层是个链表,u 的地址是同一个,链表中 u 指向的地址难道不是最后一个结构体的地址????
    main1234
        9
    main1234  
    OP
       10 天前
    @povsister 执行三次 defer ,相当于创建了 3 个链表节点,每个链表节点中 u 是同一个地址;当第一次执行 defer ,链表只有一个节点,u 指向结构体第一个元素;然后第二次执行 defer ,第一个链表节点 u 指向的元素不会变成第二个结构体元素么??
    veightz
        10
    veightz  
       10 天前
    也可以加一行

    ```golang
    u := u
    ```
    leonshaw
        11
    leonshaw  
       10 天前 via Android
    @main1234 注意 #6 说的“函数参数的 evaluate 发生在 defer 语句那一行”,包括对 receiver 的求值。第二个循环每次 u 的值不同,GetName 的 receiver 也就不同。第一个循环 u 的值不同但是地址相同,所以 GetName 的 receiver 也是相同的( 1.22 以后不是这样了)。
    fkdtz
        12
    fkdtz  
       10 天前   ❤️ 8
    我认为综合 4 、5 、6 楼的回复已经可以完整回答的楼主的问题了,不过貌似楼主有一点模糊,我根据自己的理解再具象化地补充一下,或许可以帮助楼主理解,也希望能和大家一起交流学习。
    搞清楚下面 3 个 go 的特性可能有助于理解上面的代码发生了什么:
    1.go 的自动引用和自动解引用; 2.defer 的求值时机和执行时机 3.for 循环变量只初始化一次之后一直在复用(go1.22 以前)

    第一个特性自动引用和解引用指的是,如果一个方法是定义在指针类型上的,那么你可以通过该类型的值对象来调用方法。例如代码中 func (u *Users) GetName()定义在 *User 上,但却可以在 for 循环 users1[]Users 时通过 u.GetName() 调用。这里的完整写法其实应该是 (&u).GetName()。
    自动解引用就是反过来,方法定义在值类型上,但允许你在指针类型上直接调用。

    第二个特性 defer 的执行时机大家都懂,只是需要明确的是 defer 后面语句的求值时机,是在执行到这一行时就要求值,之后压栈。

    第三个特性是循环变量 u 只初始化一次,即 u 的地址不会变(go1.22 之前),后面的循环是将新的列表元素值赋值给 u 。

    现在回头看为什么第一个 for 打出 ccc ?
    我们排除掉自动引用的干扰,还原完整写法,users1 的 for 循环中完整写法应该是 defer (&u).GetName(),执行到这里就得求值并压栈,压入的是 u 的地址,之后进入后面循环。
    由于 u 只初始化一次,所以之后的循环中 u 的地址一直不变,只是在更新他的值,所以再次执行 defer (&u).GetName() 时压入的也还是 u 的地址,就这样一共循环 3 次,压了 3 次 u 的地址,最后 u 装的是 Users("c"),所以 GetName()打出三次 c 。

    再看为什么第二个 for 打出 zyx ?
    理解了第一个 for 也就能理解第二个 for 了,这次执行 defer u.GetName() 时不需要自动引用,因为 u 本身就是*User 类型,那么此时求值压栈,压入的就是 u 的值,注意 u 是指针,虽然 u 只初始化一次 u 的地址不变,但我们压入的并不是 u 的地址,而是 u 的值,u 的值是*User("x"),也就是 User("x")的地址,接着第二次循环,压入 User("y")地址,最后压入 User("z")地址,最终执行,得到结果就是 zyx 。

    换一个角度思考,可以将 GetName 定义到 User 上,即 func (u Users) GetName(),其余代码不需要做改动,可以输出 zyxcba ,相当于 user1 循环不涉及自动引用,而 users2 循环中会自动解引用。

    我想这也是很多 for 循环中如果嵌套函数或 goroutine 时,比较推荐用函数参数传值的方式而不是闭包的原因,因为 go 全都是值传递,这样就省掉了很多变量在函数内外生命周期的问题,心智负担轻了很多。

    最后 4 楼提到在 go 1.22 版本做了修改,for 循环时循环变量已经改成每次循环都是一个全新变量了,这一点可以观察 u 的地址就能看到新版本确实每次循环都在发生变化,这样一来也就不存在上述问题了。
    body007
        13
    body007  
       10 天前   ❤️ 1
    永远记住 go 所有赋值都是值传递,能解决任何疑问。只是引用类型的变量赋值的是引用地址而已,第一个 for ,每个 u 都是对象的值传递,等于复制了一个对象。第二个 for ,每个 u 都是对象地址的值传递,等于复制了对象地址,只是第二个 u 也能通过对象地址访问字段,这是 go 的语法糖,例如 (*a).name 简写为 a.name 这样。

    当然最新的 go1.22 版本专门为 for range 特殊处理,这个版本两种 for 的 u 都是新对象,第二种也会是复制的新对象地址。
    CzaOrz
        14
    CzaOrz  
       10 天前
    1.22 以前的 for range 是复用同一变量。也就是说系统帮你内置申请了一个临时变量,举个简单的例子:

    ```

    var u Users
    for i := 0; i < len(users); i++ {
    u = users[i]

    // 以上等同于 for range
    // 以下等同于你的代码

    defer u.GetName()
    }

    ```
    kamier
        15
    kamier  
       10 天前
    Go1.22 解君愁😂
    0x90200
        16
    0x90200  
       10 天前
    第一个 u 复制了 range []Users{{"a"}, {"b"}, {"c"}} 的值, 第二个 u 复制 []*Users{{"a"}, {"b"}, {"c"}} 的地址
    dyllen
        17
    dyllen  
       10 天前
    @main1234 按照#6 说的,循环 1 u 是 Users ,执行到 defer 那行时拿到的是 u 本身的内存地址也就是&u ,每次循环都会被覆盖。循环 2 u 是*Users ,执行到 defer 那行拿的是*Users 的地址,每次 defer 都是不同的,下一次也就不会覆盖上一次的值了。
    zzhaolei
        18
    zzhaolei  
       10 天前
    #6 楼说的对。但是建议升级 go1.22 。官方已经改了,强行理解这个东西用处也不大
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2683 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 14:53 · PVG 22:53 · LAX 07:53 · JFK 10:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.