前端仔有点学不明白 golang 的 defer

155 天前
 zhengfan2016

背景:这个地方的 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))
}

感觉被绕晕了

4410 次点击
所在节点    Go 编程语言
46 条回复
coderlxm
155 天前
看来还是要多问啊,之前我这里也有疑惑但是实际没有这种写法就没管了。
mightybruce
155 天前
大家其实都是猜测, 要真深入,直接让 go 生成编译的汇编,直接查看汇编代码就好
https://gocompiler.shizhz.me/10.-golang-bian-yi-qi-han-shu-bian-yi-ji-dao-chu/10.2.1-ssa
hugozach
155 天前
使用具名返回值时,defer 修改的是返回值本身,因此能在返回之前修改返回值。
如果没有具名返回值,defer 修改的是函数中的局部变量,和返回值是两回事。返回值是在 defer 执行之后才被确定的。
szdubinbin
155 天前
我第一眼看到就觉得,这不是 useEffect 第二个返回函数的意思吗,你在这里搞有副作用(effect)的事情显然不妥吧,当然具体逻辑跟 19 楼意思差不多。
docxs
155 天前
直接看下汇编就好了:
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


这种具名返回也一样
docxs
155 天前
irrigate2554
155 天前
这 TM 纯纯八股文题目,实际你用 go 10 年也写不出这种情况的代码。
lovelylain
155 天前
你觉得别扭是因为这两个例子只是为了出题,等你遇到了合适的使用场景,就会发现 defer 的设计非常合理。例如具名返回,考虑这种场景,你要在一个处理函数里进行很多处理,最终根据是否 return err 封装回包,具名返回可以让你在 defer 里拿到的是 return 的值;还有 defer 的参数是在 defer 的时候就计算的,这样就不用担心后面对相应变量重新赋值引发的问题。
iseki
155 天前
具名返回值的这个特性可以写
defer func(){ if e!=nil{e=...}}
这样的代码,算作是没有 try...catch 和 stacktrace 的一种补偿吧。
iseki
155 天前
不要动不动去看反汇编,实际上发生了什么都能想象到,汇编只是编译器按照语言规范编译的结果而已。真正值得去探索下的是为什么语言规范要这么写,为什么语言要这么设计。
zhengfan2016
155 天前
@iseki #29 难道 golang 的 recover 不是对标 js 的 try...catch 的吗,golang 用 panic 抛出,js 用 throw 抛出,感觉 defer 更像是对标 js 的 try...finally
quantal
154 天前
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
Rehtt
154 天前
纯纯八股文,现实中这样写出现了 bug 扣你绩效
PTLin
154 天前
命名返回值是比 if err = nil 错误处理更蠢的设计
LieEar
154 天前
go 也开始 java 八股文化了
lasuar
154 天前
具名返回值定义了一个变量,既然是变量,就可以被修改。没有定义变量,就以 return 值为准。
fds
154 天前
@zhengfan2016 印象中似乎只有 python 推荐把 try catch 作为常规手段,用来让主体逻辑更简单。java 可能用的也不少? js 忘了。
Go 如果 panic 应该直接退出进程的。留个 recover 只是以防万一,比如避免第三方代码崩溃什么的,正常情况还是应该中断,然后查原因的。如果是可以处理的错误,还是应该正常返回 err ,这样更快。
defer 主要是解决 C 语言中 open() close() 需要配对使用的问题,没有 defer 可能 close() 得写好多次,很不方便,还容易遗漏。总体来讲 Go 是对 C 语言的补全,跟很多面向对象的语言思路不一样。
zhengfan2016
154 天前
@fds 对的,这个还是看情况,像 js 有些第三方库比如 zod 之类如果用户输入的值和校验类型不一致,会 throw ,有些 jwt 校验库 jwt 不合法也是会 throw ,这种肯定是希望接口返回 400 而不是 nodejs 进程直接退出了。

我不知道 golang 有没有库会在用户 post 接口输入不符合预期的时候直接 panic ,一般第三方库有 if err 肯定是用 err 的
oom
154 天前
defer 在 return 之后,函数返回结束前执行,也就是处在两者之间

1.函数无命名返回值(你的第 2 个例子),return 时,会先计算返回值,一旦计算完毕,defer 无论怎么修改,都不会影响最终返回值,但函数内部 defer 修改后的值是生效的,只是不会返回罢了

2.函数有命名返回值(第一个例子),return 时,会先计算返回值,然后将返回值赋值给命名返回值,defer 修改命名返回值,会影响最终返回值
kuanat
154 天前
我在过去几年的代码库里检索了一下,只找到了一种涉及到 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 之外没什么意义,属于某种设计失误,但也有可能是我没理解到位。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1122635

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX