golang 关于 forrange 的一些疑问 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
main1234
V2EX    程序员

golang 关于 forrange 的一些疑问

  main1234 2024-05-12 22:38:22 +08:00 2360 次点击
这是一个创建于 516 天前的主题,其中的信息可能已经有所发展或是发生改变。
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  
   2024-05-12 22:53:50 +08:00 via iPhone
在第一个 for 中,u 是 users1 数组中每个元素的副本。当使用 defer 时,scheduler 对 GetName()的调用在函数末尾执行(即 main())。重要的是,defer 捕获变量 u 本身,而不是 u 在每次迭代时指向的值。因为 u 是一个结构(不是指针),它会被循环的每次迭代覆盖,到 main()退出时,u 为循环的最后一个值,即{Name: "c"}。因此,GetName ()打印“c”三次。
gerorim
    2
gerorim  
   2024-05-12 22:54:44 +08:00 via iPhone
第二个 for ,u 是一个指针,直接指向用户 2 切片中的每个元素。同样,defer 捕获 u ,但在这里,每个 u 都是一个不同的指针,指向不同的地址。循环分别捕获每个指针,当延迟执行对 GetName()的调用时,每个指针都指向不同的用户结构。此外,由于延迟执行 LIFO ( Last In ,First Out )顺序的函数,所以应该看到“z”、“y”、“x”(循环顺序的反转),而不是楼主所要的“z”、“z”、“z”。
twl007
    3
twl007  
   2024-05-12 23:02:04 +08:00   2
Fixing For Loops in Go 1.22
https://go.dev/blog/loopvar-preview
twl007
    4
twl007  
   2024-05-12 23:03:33 +08:00 via iPhone   1
第一个行为在 1.22 修复了

在 go mid 里面的的版本小于 1.22 的时候会继续保持以前的行为 在版本大于 1.22 的时候会修正这个问题
lance6716
    5
lance6716  
   2024-05-12 23:20:34 +08:00 via Android   1
因为你的 GetName 定义在 *Users 上,当变量是类型 Users 会有一个隐含的取地址,再加上旧版本 for loop 用的是同一个地址,就会变成 ccc
povsister
    6
povsister  
   2024-05-12 23:21:36 +08:00 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  
   2024-05-12 23:25:41 +08:00 via Android
第二个 for loop:defer 并不是闭包,所以跟你说的“指向最后一个”没关系。你是直接被第一个 for loop 搞迷糊了,误以为自己掌握了某个坑能解释这个奇怪行为,其实只是瞎猫碰上死耗子

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

```golang
u := u
```
leonshaw
    11
leonshaw  
   2024-05-13 00:28:00 +08:00 via Android
@main1234 注意 #6 说的“函数参数的 evaluate 发生在 defer 语句那一行”,包括对 receiver 的求值。第二个循环每次 u 的值不同,GetName 的 receiver 也就不同。第一个循环 u 的值不同但是地址相同,所以 GetName 的 receiver 也是相同的( 1.22 以后不是这样了)。
fkdtz
    12
fkdtz  
   2024-05-13 05:18:35 +08:00   9
我认为综合 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  
   2024-05-13 09:29:38 +08:00   1
永远记住 go 所有赋值都是值传递,能解决任何疑问。只是引用类型的变量赋值的是引用地址而已,第一个 for ,每个 u 都是对象的值传递,等于复制了一个对象。第二个 for ,每个 u 都是对象地址的值传递,等于复制了对象地址,只是第二个 u 也能通过对象地址访问字段,这是 go 的语法糖,例如 (*a).name 简写为 a.name 这样。

当然最新的 go1.22 版本专门为 for range 特殊处理,这个版本两种 for 的 u 都是新对象,第二种也会是复制的新对象地址。
CzaOrz
    14
CzaOrz  
   2024-05-13 09:48:58 +08:00
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  
   2024-05-13 10:23:59 +08:00
Go1.22 解君愁
0x90200
    16
0x90200  
   2024-05-13 10:50:55 +08:00
第一个 u 复制了 range []Users{{"a"}, {"b"}, {"c"}} 的值, 第二个 u 复制 []*Users{{"a"}, {"b"}, {"c"}} 的地址
dyllen
    17
dyllen  
   2024-05-13 14:26:39 +08:00
@main1234 按照#6 说的,循环 1 u 是 Users ,执行到 defer 那行时拿到的是 u 本身的内存地址也就是&u ,每次循环都会被覆盖。循环 2 u 是*Users ,执行到 defer 那行拿的是*Users 的地址,每次 defer 都是不同的,下一次也就不会覆盖上一次的值了。
zzhaolei
    18
zzhaolei  
   2024-05-13 18:22:39 +08:00
#6 楼说的对。但是建议升级 go1.22 。官方已经改了,强行理解这个东西用处也不大
关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     892 人在线   最高记录 6679       Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 25ms UTC 20:46 PVG 04:46 LAX 13:46 JFK 16:46
Do have faith in what you're doing.
ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86