背景:这个地方的 test-1 题 https://golang.dbwu.tech/traps/defer_exam/
如下 test-1 题,使用具名返回值,defer 就能修改 t 的值
package main func foo(n int) (t int) { t = n defer func() { t += 3 }() return t } func main() { println(foo(1)) }
但是我不使用具名,就算我把 t 移到最外层的作用域,defer 也改变不了 t 的值,我试着不在 defer 作用域内,就可以修改
package main var t int func foo(n int) int { t = n defer func() { t += 3 }() return t } func main() { println(foo(1)) }
感觉被绕晕了
![]() | 1 lesismal 192 天前 ![]() 不知道谁带头搞的这些题啊,我一个都不会做、只能运行跑结果来看才知道答案。 但是我从来都不会这样用 defer 导致这种问题啊,搞这些题的人是吃得太饱了吗! |
![]() | 2 shakaraka PRO 虽然我知道有些基础应该会,不过我写了 go3 、4 年确实没碰到过这种场景。我麻了 hhhhh |
3 rahuahua 192 天前 下面这个 defer 也是能修改 t 的值,只是返回值已经拷贝了 t 的值,不受影响了 |
![]() | 4 maxwellz 192 天前 返回值如果没有设置名称,defer 中的值不会改变返回值 |
5 kcross 192 天前 给你看个好玩的,你试试这个 package main var t int func foo(n int) (t int) { t = n defer func() { t = t+ 3 }() return } func main() { println(foo(1)) } |
![]() | 6 uion 192 天前 不会 go ,盲猜一下。参数有引用,具名参数返回时先运行 defer 。不使用具名应该是直接返回了再 defer ? |
7 zhengfan2016 OP @maxwellz 对的,我就想问这个问题,为什么不设置名称 defer 就改不动呢 |
8 R136a1 192 天前 值传递和引用传递的区别? |
9 ninjashixuan 192 天前 想学这类边界技巧可以关注 go101 的作者。 |
10 NessajCN 192 天前 https://go.dev/blog/defer-panic-and-recover "3. Deferred functions may read and assign to the returning function's named return values." 纯粹就是 named return 特性,死记就好了 |
![]() | 11 sardina 192 天前 谁要在开发中这么写代码 小心被打 |
12 zhengfan2016 OP @sardina 哈哈,我感觉你可以整理一个 awesome golang 容易挨打的代码片段,让新手村的 xdm 学习 ![]() |
13 vincentWdp 192 天前 ``` func foo(n int) (t int) { t = n ``` 换成 ``` func foo(n int) int { t := n ``` |
![]() | 14 ChrisFreeMan 192 天前 @lesismal 看到你也不会我就放心了 |
15 zhengfan2016 OP @NessajCN 原来如此 |
![]() | 16 sardina 192 天前 ![]() 第一个例子 return 是先把返回值存到临时变量里,然后 defer 再修改也改不到临时变量 第一个例子因为返回值有命名,所以 return 是把返回值存到这个命名里里,然后 defer 就可以修改了 总的来说就是 return 先设置返回值 然后再执行 defer ,然后函数返回 https://www.cnblogs.com/saryli/p/11371912.html 可以看这个 |
![]() | 17 peteretep 192 天前 后端仔都不这么写的。不要起步就走犄角旮旯了。没有实际意义的。 这个和 c++考试 i++++ 、 ++i++ 的题目有什么区别吗? defer 只用来释放资源,其他使用正常的程序算法解决。 |
![]() | 18 maxwellz 192 天前 @zhengfan2016 #7 貌似和 defer 的特性有关系了,这块太久没看了,忘了 |
![]() | 19 k0m8MNz2Ywf0OLeH 192 天前 很简单啊,defer 放到 一个栈里面。 defer func() { t+=3}() 这个匿名函数 放入到栈中, 等 function 结束时运行。 第一个 函数返回 t, 函数结束后继续执行了 t+=3 。 第二个 函数返回 t 的值,数据结束后继续执行了 t+=3, 此时的 t 和函数返回结果 没有关系。 |
![]() | 20 k0m8MNz2Ywf0OLeH 192 天前 @wunonglin #2 我代码经常写 defer ,比如打开文件需要 close, 或者回收 chan, 还有数据库事物结束。 |
![]() | 21 coderlxm 192 天前 via Android 看来还是要多问啊,之前我这里也有疑惑但是实际没有这种写法就没管了。 |
![]() | 22 mightybruce 192 天前 大家其实都是猜测, 要真深入,直接让 go 生成编译的汇编,直接查看汇编代码就好 https://gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa |
![]() | 23 hugozach 192 天前 使用具名返回值时,defer 修改的是返回值本身,因此能在返回之前修改返回值。 如果没有具名返回值,defer 修改的是函数中的局部变量,和返回值是两回事。返回值是在 defer 执行之后才被确定的。 |
![]() | 24 szdubinbin 192 天前 我第一眼看到就觉得,这不是 useEffect 第二个返回函数的意思吗,你在这里搞有副作用(effect)的事情显然不妥吧,当然具体逻辑跟 19 楼意思差不多。 |
25 docxs 192 天前 ![]() |
26 docxs 192 天前 |
![]() | 27 irrigate2554 192 天前 ![]() 这 TM 纯纯八股文题目,实际你用 go 10 年也写不出这种情况的代码。 |
28 lovelylain 192 天前 via Android 你觉得别扭是因为这两个例子只是为了出题,等你遇到了合适的使用场景,就会发现 defer 的设计非常合理。例如具名返回,考虑这种场景,你要在一个处理函数里进行很多处理,最终根据是否 return err 封装回包,具名返回可以让你在 defer 里拿到的是 return 的值;还有 defer 的参数是在 defer 的时候就计算的,这样就不用担心后面对相应变量重新赋值引发的问题。 |
29 iseki 192 天前 via Android 具名返回值的这个特性可以写 defer func(){ if e!=nil{e=...}} 这样的代码,算作是没有 try...catch 和 stacktrace 的一种补偿吧。 |
30 iseki 192 天前 via Android ![]() 不要动不动去看反汇编,实际上发生了什么都能想象到,汇编只是编译器按照语言规范编译的结果而已。真正值得去探索下的是为什么语言规范要这么写,为什么语言要这么设计。 |
31 zhengfan2016 OP @iseki #29 难道 golang 的 recover 不是对标 js 的 try...catch 的吗,golang 用 panic 抛出,js 用 throw 抛出,感觉 defer 更像是对标 js 的 try...finally ![]() |
32 quantal 191 天前 defer 的用法总结了三条规则 #### defer 不能修改非具名返回值,可以修改具名返回值,具名返回值进入函数时为 0 #### defer 传入的参数定义时确定,执行不与定义同步进行 #### defer 执行时机:return 执行后,函数真正的返回前执行,LIFO func foo() (t int) { defer func(n int) { println(n) println(t) t = 9 }(t) t = 1 return 2 } func main() { println("result:", foo()) 结果是: 0 2 result: 9 |
![]() | 33 Rehtt 191 天前 纯纯八股文,现实中这样写出现了 bug 扣你绩效 |
![]() | 34 PTLin 191 天前 命名返回值是比 if err = nil 错误处理更蠢的设计 |
![]() | 35 LieEar 191 天前 go 也开始 java 八股文化了 |
![]() | 36 lasuar 191 天前 具名返回值定义了一个变量,既然是变量,就可以被修改。没有定义变量,就以 return 值为准。 |
37 fds 191 天前 @zhengfan2016 印象中似乎只有 python 推荐把 try catch 作为常规手段,用来让主体逻辑更简单。java 可能用的也不少? js 忘了。 Go 如果 panic 应该直接退出进程的。留个 recover 只是以防万一,比如避免第三方代码崩溃什么的,正常情况还是应该中断,然后查原因的。如果是可以处理的错误,还是应该正常返回 err ,这样更快。 defer 主要是解决 C 语言中 open() close() 需要配对使用的问题,没有 defer 可能 close() 得写好多次,很不方便,还容易遗漏。总体来讲 Go 是对 C 语言的补全,跟很多面向对象的语言思路不一样。 |
38 zhengfan2016 OP @fds 对的,这个还是看情况,像 js 有些第三方库比如 zod 之类如果用户输入的值和校验类型不一致,会 throw ,有些 jwt 校验库 jwt 不合法也是会 throw ,这种肯定是希望接口返回 400 而不是 nodejs 进程直接退出了。 我不知道 golang 有没有库会在用户 post 接口输入不符合预期的时候直接 panic ,一般第三方库有 if err 肯定是用 err 的 ![]() |
39 oom 191 天前 defer 在 return 之后,函数返回结束前执行,也就是处在两者之间 1.函数无命名返回值(你的第 2 个例子),return 时,会先计算返回值,一旦计算完毕,defer 无论怎么修改,都不会影响最终返回值,但函数内部 defer 修改后的值是生效的,只是不会返回罢了 2.函数有命名返回值(第一个例子),return 时,会先计算返回值,然后将返回值赋值给命名返回值,defer 修改命名返回值,会影响最终返回值 |
![]() | 40 kuanat 191 天前 我在过去几年的代码库里检索了一下,只找到了一种涉及到 defer 里面修改返回值操作的反例。严格来说,这个代码编写方式是 named return 的问题,而不是 defer 的问题。 前面提到的 defer 里修改返回值的情况是: // fn 函数签名 fn() (err error) defer func() { err = writer.Close() }() 这样就会覆盖掉原本 err ,所以还要新增变量特殊处理一下。 defer func() { closeErr := writer.Close() if closeErr != nil { // 特殊处理 } }() 这样看起来就很蠢对吧,所以代码规范里就直接禁止了在 defer 里写逻辑。我确实想象不出来正常的业务代码里有什么一定非要在 defer 里处理的逻辑不可。个人的观点是,这个和三元逻辑操作符差不多,都是不适合工程上团队协作使用的。 当然我这里的规范还有一条,interface 写 named return ,这样注释可以对应到参数名。 我印象有个说法是 go 早期是手搓编译器,named return 能方便代码生成。其实我觉得这个特性除了支持 naked return 之外没什么意义,属于某种设计失误,但也有可能是我没理解到位。 |
41 Feiir 191 天前 ![]() 你只要记住当你声明 (t int) 作为返回值时,t 是一个与返回值绑定的变量就行了,没有具名返回值的话,t 就没和返回值绑定,在返回的那一刻就确定值了。 |
![]() | 42 lxdlam 191 天前 这个问题非常得“巧妙”,因为他混合了三个东西,把这个结果拖向了一个“记住就行”的深渊。 1. 返回值的处理:根据 Go 关于 [Return Value]( https://go.dev/ref/spec#Return_statements) 的规范,当你声明一个返回值的时候,你实际上是声明了一个临时对象,区别仅存在于这个返回对象是有名字还是没有名字的; 2. Defer 的作用时机:根据 Go 关于 [Defer Statement]( https://go.dev/ref/spec#Defer_statements) 的规范,`defer` 的作用时机在 `return` 的所有 Value 都被计算且赋值完毕后,真实返回前执行; 3. Go 对闭包的处理:根据 Go 关于 [Function literals]( https://go.dev/ref/spec#Function_literals) 的规范,闭包内捕获的自由变量会被共享;换句话说,你可以理解为闭包实际上捕获了外部变量的指针,对其的修改会同步到原始对象。 花点时间理解上面三个规范会带来的代码作用。 现在我们来分析代码: - 在 Case1 中,由于返回值被具名了,`return t` 实际上可以理解为 `t = t; return`,也就是仅仅重新赋值,之后执行的 `defer` 重新修改了 `t`,导致返回的值产生了变动。 - 在 Case2 中,由于返回值匿名,假定返回值是一个隐藏变量 `tForReturn`,`return t` 实际上可以理解为 `tForReturn = t; return`,此时虽然你的 `defer` 修改了 `t`,但是由于返回的对象是 `tForReturn`,获取的返回值并没有发生变化,一切正常。当然,此时你再次在 `foo` 调用后查看 `t` 的值,它确实也会是 `4`,`defer` 的作用生效了。 P.S. Go 的规范对这种行为没有明确的规定,上面的三个 spec 其实也只能说是“模糊”描述了作用原理,还是要观察编译器的实现实锤,这也是这门语言天天开天窗擦屁股的核心包袱之一;具名返回值这个特性本身也有很强的“拍脑袋”属性,它确实有用,但是没有有用到这个程度,结果反而引入了更多的混淆。 |
![]() | 43 DOGOOD 191 天前 别学了,这种代码再学下去脑子该坏了。 |
![]() | 44 duzhuo 191 天前 人肉编译器哈哈 |
![]() | 45 onnethy 190 天前 说实话啊 这两段代码直接扔到 ds 里,给你讲的明明白白了 虽然我看也你的代码 也绕得很 但是 ds 倒是给我讲明白了 |
![]() | 46 whinyStone 185 天前 具名返回那种应该是在栈内存提前指定了返回值的位置,所以 defer 执行的时候可以改到那个位置; 后面的返回值一般可能是寄存器传递的,汇编层面会是先算出来,然后执行 defer 的函数,然后弹栈返回; 即使是栈内存传递,也会有一次复制,defer 仍改不到返回值,可以改一个大数组看看; 堆内存就不一样了,在 foo 里声明一个切片并返回它,在 defer 里做修改,返回后的拿到的切片当然是修改后的 |