前端仔有点学不明白 golang 的 defer
背景:这个地方的 test-1 题 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))
}
感觉被绕晕了
不知道谁带头搞的这些题啊,我一个都不会做、只能运行跑结果来看才知道答案。
但是我从来都不会这样用 defer 导致这种问题啊,搞这些题的人是吃得太饱了吗!
虽然我知道有些基础应该会,不过我写了 go3 、4 年确实没碰到过这种场景。我麻了 hhhhh
下面这个 defer 也是能修改 t 的值,只是返回值已经拷贝了 t 的值,不受影响了
返回值如果没有设置名称,defer 中的值不会改变返回值
给你看个好玩的,你试试这个
package main
var t int
func foo(n int) (t int) {
t = n
defer func() {
t = t+ 3
}()
return
}
func main() {
println(foo(1))
}
不会 go ,盲猜一下。参数有引用,具名参数返回时先运行 defer 。不使用具名应该是直接返回了再 defer ?
对的,我就想问这个问题,为什么不设置名称 defer 就改不动呢
值传递和引用传递的区别?
想学这类边界技巧可以关注 go101 的作者。
go.dev/blog/defer-panic-and-recover
"3. Deferred functions may read and assign to the returning function's named return values."
纯粹就是 named return 特性,死记就好了
谁要在开发中这么写代码 小心被打
哈哈,我感觉你可以整理一个 awesome golang 容易挨打的代码片段,让新手村的 xdm 学习
func foo(n int) (t int) {
t = n
换成
func foo(n int) int {
t := n
看到你也不会我就放心了
原来如此
第一个例子 return 是先把返回值存到临时变量里,然后 defer 再修改也改不到临时变量
第一个例子因为返回值有命名,所以 return 是把返回值存到这个命名里里,然后 defer 就可以修改了
总的来说就是 return 先设置返回值 然后再执行 defer ,然后函数返回
www.cnblogs.com/saryli/p/11371912.html 可以看这个
后端仔都不这么写的。不要起步就走犄角旮旯了。没有实际意义的。
这个和 c++考试 i++++ 、 ++i++ 的题目有什么区别吗?
defer 只用来释放资源,其他使用正常的程序算法解决。
#7 貌似和 defer 的特性有关系了,这块太久没看了,忘了
很简单啊,defer 放到 一个栈里面。
defer func() { t+=3}() 这个匿名函数 放入到栈中, 等 function 结束时运行。
第一个 函数返回 t, 函数结束后继续执行了 t+=3 。
第二个 函数返回 t 的值,数据结束后继续执行了 t+=3, 此时的 t 和函数返回结果 没有关系。
#2 我代码经常写 defer ,比如打开文件需要 close, 或者回收 chan, 还有数据库事物结束。
看来还是要多问啊,之前我这里也有疑惑但是实际没有这种写法就没管了。
大家其实都是猜测, 要真深入,直接让 go 生成编译的汇编,直接查看汇编代码就好
gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa
使用具名返回值时,defer 修改的是返回值本身,因此能在返回之前修改返回值。
如果没有具名返回值,defer 修改的是函数中的局部变量,和返回值是两回事。返回值是在 defer 执行之后才被确定的。
我第一眼看到就觉得,这不是 useEffect 第二个返回函数的意思吗,你在这里搞有副作用(effect)的事情显然不妥吧,当然具体逻辑跟 19 楼意思差不多。
直接看下汇编就好了:
test-1 具名返回,0x8(SP)就是 t ,defer 里也会修改这个地址的值,最后 MOVQ 0x8(SP), AX 再给返回值,另外 return 的时候有没有 t 都一样
test-2 不具名,先是把 t 的值 0x10(SP)给了返回暂存值 0x8(SP),然后执行 defer ,执行完再把暂存值 0x8(SP)给到 AX 做返回值,在 defer 里改的是 0x10(SP),并未改到 0x8(SP),所以返回值是最初的 t
这种具名返回也一样
这 TM 纯纯八股文题目,实际你用 go 10 年也写不出这种情况的代码。
你觉得别扭是因为这两个例子只是为了出题,等你遇到了合适的使用场景,就会发现 defer 的设计非常合理。例如具名返回,考虑这种场景,你要在一个处理函数里进行很多处理,最终根据是否 return err 封装回包,具名返回可以让你在 defer 里拿到的是 return 的值;还有 defer 的参数是在 defer 的时候就计算的,这样就不用担心后面对相应变量重新赋值引发的问题。
具名返回值的这个特性可以写
defer func(){ if e!=nil{e=...}}
这样的代码,算作是没有 try...catch 和 stacktrace 的一种补偿吧。
不要动不动去看反汇编,实际上发生了什么都能想象到,汇编只是编译器按照语言规范编译的结果而已。真正值得去探索下的是为什么语言规范要这么写,为什么语言要这么设计。
#29 难道 golang 的 recover 不是对标 js 的 try...catch 的吗,golang 用 panic 抛出,js 用 throw 抛出,感觉 defer 更像是对标 js 的 try...finally
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
纯纯八股文,现实中这样写出现了 bug 扣你绩效
命名返回值是比 if err = nil 错误处理更蠢的设计
go 也开始 java 八股文化了
具名返回值定义了一个变量,既然是变量,就可以被修改。没有定义变量,就以 return 值为准。
印象中似乎只有 python 推荐把 try catch 作为常规手段,用来让主体逻辑更简单。java 可能用的也不少? js 忘了。
Go 如果 panic 应该直接退出进程的。留个 recover 只是以防万一,比如避免第三方代码崩溃什么的,正常情况还是应该中断,然后查原因的。如果是可以处理的错误,还是应该正常返回 err ,这样更快。
defer 主要是解决 C 语言中 open() close() 需要配对使用的问题,没有 defer 可能 close() 得写好多次,很不方便,还容易遗漏。总体来讲 Go 是对 C 语言的补全,跟很多面向对象的语言思路不一样。
对的,这个还是看情况,像 js 有些第三方库比如 zod 之类如果用户输入的值和校验类型不一致,会 throw ,有些 jwt 校验库 jwt 不合法也是会 throw ,这种肯定是希望接口返回 400 而不是 nodejs 进程直接退出了。
我不知道 golang 有没有库会在用户 post 接口输入不符合预期的时候直接 panic ,一般第三方库有 if err 肯定是用 err 的
defer 在 return 之后,函数返回结束前执行,也就是处在两者之间
1.函数无命名返回值(你的第 2 个例子),return 时,会先计算返回值,一旦计算完毕,defer 无论怎么修改,都不会影响最终返回值,但函数内部 defer 修改后的值是生效的,只是不会返回罢了
2.函数有命名返回值(第一个例子),return 时,会先计算返回值,然后将返回值赋值给命名返回值,defer 修改命名返回值,会影响最终返回值
我在过去几年的代码库里检索了一下,只找到了一种涉及到 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 之外没什么意义,属于某种设计失误,但也有可能是我没理解到位。
你只要记住当你声明 (t int) 作为返回值时,t 是一个与返回值绑定的变量就行了,没有具名返回值的话,t 就没和返回值绑定,在返回的那一刻就确定值了。
这个问题非常得“巧妙”,因为他混合了三个东西,把这个结果拖向了一个“记住就行”的深渊。
- 返回值的处理:根据 Go 关于 Return Value 的规范,当你声明一个返回值的时候,你实际上是声明了一个临时对象,区别仅存在于这个返回对象是有名字还是没有名字的;
- Defer 的作用时机:根据 Go 关于 Defer Statement 的规范,
defer
的作用时机在return
的所有 Value 都被计算且赋值完毕后,真实返回前执行; - Go 对闭包的处理:根据 Go 关于 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 其实也只能说是“模糊”描述了作用原理,还是要观察编译器的实现实锤,这也是这门语言天天开天窗擦屁股的核心包袱之一;具名返回值这个特性本身也有很强的“拍脑袋”属性,它确实有用,但是没有有用到这个程度,结果反而引入了更多的混淆。
别学了,这种代码再学下去脑子该坏了。
人肉编译器哈哈
说实话啊 这两段代码直接扔到 ds 里,给你讲的明明白白了
虽然我看也你的代码 也绕得很 但是 ds 倒是给我讲明白了
某人用Java搞了一个流体力学的演示。 http://grantkot.com/MPM/Liquid.html 不过,这仅仅是个开始。某同学将其发布上了reddit.com,…
在Stack Overflow上有这样的一个贴子《What’s your most controversial programming opinion?》,翻译成中文就是“你认…
需求:(优先级逐次递减) 1.存储孩子视频和文件 2.影音库 3.游戏库(各种模拟器) 4.文件库。 以下问题皆基于以上需求而提出。 问题: 1. 各个 NAS 品牌之间的区…