背景:这个地方的 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 3 天前 ![]() 不知道谁带头搞的这些题啊,我一个都不会做、只能运行跑结果来看才知道答案。
但是我从来都不会这样用 defer 导致这种问题啊,搞这些题的人是吃得太饱了吗! |
![]() |
2
wunonglin 3 天前
虽然我知道有些基础应该会,不过我写了 go3 、4 年确实没碰到过这种场景。我麻了 hhhhh
|
3
rahuahua 3 天前
下面这个 defer 也是能修改 t 的值,只是返回值已经拷贝了 t 的值,不受影响了
|
![]() |
4
maxwellz 3 天前
返回值如果没有设置名称,defer 中的值不会改变返回值
|
5
kcross 3 天前
给你看个好玩的,你试试这个
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 3 天前
不会 go ,盲猜一下。参数有引用,具名参数返回时先运行 defer 。不使用具名应该是直接返回了再 defer ?
|
7
zhengfan2016 OP @maxwellz 对的,我就想问这个问题,为什么不设置名称 defer 就改不动呢
|
8
R136a1 3 天前
值传递和引用传递的区别?
|
9
ninjashixuan 3 天前
想学这类边界技巧可以关注 go101 的作者。
|
10
NessajCN 3 天前
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 3 天前
谁要在开发中这么写代码 小心被打
|
12
zhengfan2016 OP @sardina 哈哈,我感觉你可以整理一个 awesome golang 容易挨打的代码片段,让新手村的 xdm 学习
![]() |
13
vincentWdp 3 天前
```
func foo(n int) (t int) { t = n ``` 换成 ``` func foo(n int) int { t := n ``` |
![]() |
14
ChrisFreeMan 3 天前
@lesismal 看到你也不会我就放心了
|
15
zhengfan2016 OP @NessajCN 原来如此
|
16
sardina 3 天前 ![]() 第一个例子 return 是先把返回值存到临时变量里,然后 defer 再修改也改不到临时变量
第一个例子因为返回值有命名,所以 return 是把返回值存到这个命名里里,然后 defer 就可以修改了 总的来说就是 return 先设置返回值 然后再执行 defer ,然后函数返回 https://www.cnblogs.com/saryli/p/11371912.html 可以看这个 |
![]() |
17
peteretep 3 天前
后端仔都不这么写的。不要起步就走犄角旮旯了。没有实际意义的。
这个和 c++考试 i++++ 、 ++i++ 的题目有什么区别吗? defer 只用来释放资源,其他使用正常的程序算法解决。 |
![]() |
18
maxwellz 3 天前
@zhengfan2016 #7 貌似和 defer 的特性有关系了,这块太久没看了,忘了
|
![]() |
19
Liv1Dad 3 天前
很简单啊,defer 放到 一个栈里面。
defer func() { t+=3}() 这个匿名函数 放入到栈中, 等 function 结束时运行。 第一个 函数返回 t, 函数结束后继续执行了 t+=3 。 第二个 函数返回 t 的值,数据结束后继续执行了 t+=3, 此时的 t 和函数返回结果 没有关系。 |
![]() |
21
coderlxm 3 天前 via Android
看来还是要多问啊,之前我这里也有疑惑但是实际没有这种写法就没管了。
|
![]() |
22
mightybruce 3 天前
大家其实都是猜测, 要真深入,直接让 go 生成编译的汇编,直接查看汇编代码就好
https://gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa |
![]() |
23
hugozach 3 天前
使用具名返回值时,defer 修改的是返回值本身,因此能在返回之前修改返回值。
如果没有具名返回值,defer 修改的是函数中的局部变量,和返回值是两回事。返回值是在 defer 执行之后才被确定的。 |
![]() |
24
szdubinbin 3 天前
我第一眼看到就觉得,这不是 useEffect 第二个返回函数的意思吗,你在这里搞有副作用(effect)的事情显然不妥吧,当然具体逻辑跟 19 楼意思差不多。
|
25
docxs 3 天前 ![]() |
26
docxs 3 天前
|
![]() |
27
xausky 3 天前 ![]() 这 TM 纯纯八股文题目,实际你用 go 10 年也写不出这种情况的代码。
|
28
lovelylain 3 天前 via Android
你觉得别扭是因为这两个例子只是为了出题,等你遇到了合适的使用场景,就会发现 defer 的设计非常合理。例如具名返回,考虑这种场景,你要在一个处理函数里进行很多处理,最终根据是否 return err 封装回包,具名返回可以让你在 defer 里拿到的是 return 的值;还有 defer 的参数是在 defer 的时候就计算的,这样就不用担心后面对相应变量重新赋值引发的问题。
|
29
iseki 3 天前 via Android
具名返回值的这个特性可以写
defer func(){ if e!=nil{e=...}} 这样的代码,算作是没有 try...catch 和 stacktrace 的一种补偿吧。 |
30
iseki 3 天前 via Android ![]() 不要动不动去看反汇编,实际上发生了什么都能想象到,汇编只是编译器按照语言规范编译的结果而已。真正值得去探索下的是为什么语言规范要这么写,为什么语言要这么设计。
|
31
zhengfan2016 OP @iseki #29 难道 golang 的 recover 不是对标 js 的 try...catch 的吗,golang 用 panic 抛出,js 用 throw 抛出,感觉 defer 更像是对标 js 的 try...finally
![]() |
32
quantal 2 天前
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 2 天前
纯纯八股文,现实中这样写出现了 bug 扣你绩效
|
![]() |
34
PTLin 2 天前
命名返回值是比 if err = nil 错误处理更蠢的设计
|
![]() |
35
LieEar 2 天前
go 也开始 java 八股文化了
|
![]() |
36
lasuar 2 天前
具名返回值定义了一个变量,既然是变量,就可以被修改。没有定义变量,就以 return 值为准。
|
37
fds 2 天前
@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 2 天前
defer 在 return 之后,函数返回结束前执行,也就是处在两者之间
1.函数无命名返回值(你的第 2 个例子),return 时,会先计算返回值,一旦计算完毕,defer 无论怎么修改,都不会影响最终返回值,但函数内部 defer 修改后的值是生效的,只是不会返回罢了 2.函数有命名返回值(第一个例子),return 时,会先计算返回值,然后将返回值赋值给命名返回值,defer 修改命名返回值,会影响最终返回值 |
![]() |
40
kuanat 2 天前
我在过去几年的代码库里检索了一下,只找到了一种涉及到 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 2 天前 ![]() 你只要记住当你声明 (t int) 作为返回值时,t 是一个与返回值绑定的变量就行了,没有具名返回值的话,t 就没和返回值绑定,在返回的那一刻就确定值了。
|
![]() |
42
lxdlam 2 天前
这个问题非常得“巧妙”,因为他混合了三个东西,把这个结果拖向了一个“记住就行”的深渊。
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 2 天前
别学了,这种代码再学下去脑子该坏了。
|
![]() |
44
duzhuo 2 天前
人肉编译器哈哈
|
![]() |
45
onnethy 1 天前
说实话啊 这两段代码直接扔到 ds 里,给你讲的明明白白了
虽然我看也你的代码 也绕得很 但是 ds 倒是给我讲明白了 |