千家信息网

如何正确使用Go defer

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,这篇文章主要介绍"如何正确使用Go defer",在日常操作中,相信很多人在如何正确使用Go defer问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"如何正确使用Go
千家信息网最后更新 2026年11月02日如何正确使用Go defer

这篇文章主要介绍"如何正确使用Go defer",在日常操作中,相信很多人在如何正确使用Go defer问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"如何正确使用Go defer"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

在 Go 语言中 defer 是一个非常有意思的关键字特性。例子如下:

package main  import "fmt"  func main() {  defer fmt.Println("煎鱼了")   fmt.Println("脑子进") }

输出结果是:

脑子进 煎鱼了

在前几天我的读者群内有小伙伴讨论起了下面这个问题:

读者群的聊天截图

简单来讲,问题就是针对在 for 循环里搞 defer 关键字,是否会造成什么性能影响?

因为在 Go 语言的底层数据结构设计上 defer 是链表的数据结构:

defer 基本底层结构

大家担心如果循环过大 defer 链表会巨长,不够 "精益求精"。又或是猜想会不会 Go defer 的设计和 Redis 数据结构设计类似,自己做了优化,其实没啥大影响?

今天这篇文章,我们就来探索循环 Go defer,造成底层链表过长会不会带来什么问题,若有,具体有什么影响?

开始吸鱼之路。

defer 性能优化 30%

在早年 Go1.13 时曾经对 defer 进行了一轮性能优化,在大部分场景下 提高了 defer 30% 的性能:

Go defer 1.13 优化记录

我们来回顾一下 Go1.13 的变更,看看 Go defer 优化在了哪里,这是问题的关键点。

以前和现在对比

在 Go1.12 及以前,调用 Go defer 时汇编代码如下:

0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)  0x0075 00117 (main.go:6)    TESTL    AX, AX  0x0077 00119 (main.go:6)    JNE    137  0x0079 00121 (main.go:7)    XCHGL    AX, AX  0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)  0x007f 00127 (main.go:7)    MOVQ    56(SP), BP

在 Go1.13 及以后,调用 Go defer 时汇编代码如下:

0x006e 00110 (main.go:4) MOVQ AX, (SP) 0x0072 00114 (main.go:4) CALL runtime.deferprocStack(SB) 0x0077 00119 (main.go:4) TESTL AX, AX 0x0079 00121 (main.go:4) JNE 139 0x007b 00123 (main.go:7) XCHGL AX, AX 0x007c 00124 (main.go:7) CALL runtime.deferreturn(SB) 0x0081 00129 (main.go:7) MOVQ 112(SP), BP

从汇编的角度来看,像是原本调用 runtime.deferproc 方法改成了调用 runtime.deferprocStack 方法,难道是做了什么优化?

我们抱着疑问继续看下去。

defer 最小单元:_defer

相较于以前的版本,Go defer 的最小单元 _defer 结构体主要是新增了 heap 字段:

type _defer struct {  siz     int32  siz     int32 // includes both arguments and results  started bool  heap    bool  sp      uintptr // sp at time of defer  pc      uintptr  fn      *funcval  ...

该字段用于标识这个 _defer 是在堆上,还是在栈上进行分配,其余字段并没有明确变更,那我们可以把聚焦点放在 defer 的堆栈分配上了,看看是做了什么事。

deferprocStack

func deferprocStack(d *_defer) {  gp := getg()  if gp.m.curg != gp {   throw("defer on system stack")  }    d.started = false  d.heap = false  d.sp = getcallersp()  d.pc = getcallerpc()   *(*uintptr)(unsafe.Pointer(&d._panic)) = 0  *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))  *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))   return0() }

这一块代码挺常规的,主要是获取调用 defer 函数的函数栈指针、传入函数的参数具体地址以及PC(程序计数器),这块在前文 《深入理解 Go defer》 有详细介绍过,这里就不再赘述了。

这个 deferprocStack 特殊在哪呢?

可以看到它把 d.heap 设置为了 false,也就是代表 deferprocStack 方法是针对将 _defer 分配在栈上的应用场景的。

deferproc

问题来了,它又在哪里处理分配到堆上的应用场景呢?

func newdefer(siz int32) *_defer {  ...  d.heap = true  d.link = gp._defer  gp._defer = d  return d }

具体的 newdefer 是在哪里调用的呢,如下:

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn  ...  sp := getcallersp()  argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)  callerpc := getcallerpc()   d := newdefer(siz)  ... }

非常明确,先前的版本中调用的 deferproc 方法,现在被用于对应分配到堆上的场景了。

小结

  • 可以确定的是 deferproc 并没有被去掉,而是流程被优化了。

  • Go 编译器会根据应用场景去选择使用 deferproc 还是 deferprocStack 方法,他们分别是针对分配在堆上和栈上的使用场景。

优化在哪儿

主要优化在于其 defer 对象的堆栈分配规则的改变,措施是:编译器对 defer 的 for-loop 迭代深度进行分析。

// src/cmd/compile/internal/gc/esc.go case ODEFER:  if e.loopdepth == 1 { // top level   n.Esc = EscNever // force stack allocation of defer record (see ssa.go)   break  }

如果 Go 编译器检测到循环深度(loopdepth)为 1,则设置逃逸分析的结果,将分配到栈上,否则分配到堆上。

// src/cmd/compile/internal/gc/ssa.go case ODEFER:  d := callDefer  if n.Esc == EscNever {   d = callDeferStack  }  s.call(n.Left, d)

以此免去了以前频繁调用 systemstack、mallocgc 等方法所带来的大量性能开销,来达到大部分场景提高性能的作用。

循环调用 defer

回到问题本身,知道了 defer 优化的原理后。那 "循环里搞 defer 关键字,是否会造成什么性能影响?"

最直接的影响就是这大约 30% 的性能优化直接全无,且由于姿势不正确,理论上 defer 既有的开销(链表变长)也变大,性能变差。

因此我们要避免以下两种场景的代码:

  • 显式循环:在调用 defer 关键字的外层有显式的循环调用,例如:for-loop 语句等。

  • 隐式循环:在调用 defer 关键字有类似循环嵌套的逻辑,例如:goto 语句等。

显式循环

第一个例子是直接在代码的 for 循环中使用 defer 关键字:

func main() {  for i := 0; i <= 99; i++ {   defer func() {    fmt.Println("脑子进煎鱼了")   }()  } }

这个也是最常见的模式,无论是写爬虫时,又或是 Goroutine 调用时,不少人都喜欢这么写。

这属于显式的调用了循环。

隐式循环

第二个例子是在代码中使用类似 goto 关键字:

func main() {  i := 1 food:  defer func() {}()  if i == 1 {   i -= 1   goto food  } }

这种写法比较少见,因为 goto 关键字有时候甚至会被列为代码规范不给使用,主要是会造成一些滥用,所以大多数就选择其实方式实现逻辑。

这属于隐式的调用,造成了类循环的作用。

到此,关于"如何正确使用Go defer"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

循环 关键 性能 分配 关键字 场景 代码 方法 问题 结构 影响 学习 例子 函数 字段 底层 数据 是在 编译器 脑子 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 上海挚品互联网科技有限公司 广东万易互联网科技有限公司惠州分公司 互联网科技公司起名 广州百单互联网科技有限公司 互联网科技股票 西安互联网科技公司 互联网科技英文翻译 深圳卡尔迅互联网科技有限公司怎么样 哈尔滨秒房互联网科技有限公司 承德科技产业互联网发展 互联网科技公司实习生 996高科技互联网公司 互联网科技的全部知识 威龙国际互联网科技公司 互联网科技圈年度人物 北京市互联网新科技 互联网科技股为什么大涨 易企秀互联网科技是做什么的 绵阳真晨互联网科技有限公司 高科技和互联网的现代国家 新华互联网科技 科技互联网政务服务 互联网科技卖菜 现代科技和互联网有关系吗 银川新华互联网科技职业技术培训学校图片 广州新华互联网科技学校电竞方面 互联网科技创新的路径 互联网科技平台产品运营 互联网时代高科技布局 互联网科技快速发展优美句子
0