defer学习指南
一、源头:
早期管理资源(如数据库连接、锁、文件句柄、网络连接)和状态清理异常麻烦。
必须在每个可能的返回点(return、err、panic)手动重复清理代码,极易遗漏且打断主要逻辑思路!
像Java语言虽然用了Try-Catch,但缺点是逻辑不清晰、臃肿、不容易判断错误出在什么地方。
作为新生代的Go,及众多语言之精华,推出了defer处理机制。
尤其是在Go1.14版本时,性能开销接近零,更无后顾之忧。
二、定义:
1、语法:
defer functionCall(..arguments..)
defer后面直接跟一个函数调用(可以是命名函数/匿名函数/方法...)
2、定义:
当函数执行到defer语句时(注册时),他会立即求值此时该函数调用的参数并将此次函数调用(包括已求值的参数)放到一个延迟调用表中。这个调用函数与goroutine关联,采用LIFO(后进先出的方式调用)。切记这个延迟调用表不会立即执行,而是会等到(函数真正结束之前--函数(return或panic)之后)在调用。
3、特性:
1、延迟执行:运行到defer时,只是将求值后的参数与调用的函数一并打包到延迟调用表中,需等到函数体结束之后在执行
2、LIFO方式执行:后进先出的方式执行
3、参数求值时机:defer 语句中的函数参数的值,是在执行到defer语句时(即注册时)就确定并保存下来的,而不是在延迟函数实际执行时才求值。
4、作用域:自各函数体
三、应用:
1、临近释放:(逻辑清晰)
mu.Lock()defer mu.Unlock() // 好习惯!确保解锁// ... 操作共享数据 ...
2、panic补获:(防止程序崩溃)
特殊情况,根据源码分析---协程中出现panic,若不能再该协程中捕获,则会导致整个程序崩溃。
func test(){ defer func(){ if r := recover(); r!=nil{ fmt.Println(r); } }() panic(1);}
3、循环函数释放:(利用完资源后,及时释放资源)
// 正确做法:将文件处理封装到函数,defer 在每次循环的匿名函数结束时执行func outerFunc() { for _, filename := range filenames { func() { // 匿名函数 f, err := os.Open(filename) if err != nil { log.Println(err) return // 退出匿名函数 } defer f.Close() // 延迟到当前匿名函数结束时执行 (即本次循环结束) // ... 处理 f ... }() // 立即调用匿名函数 }}
4、查看执行顺序:
代码右上角,有个运行小按钮,点击运行查看。
package mainimport (\"fmt\"\"log\")func g(i int) {if i > 1 {fmt.Println(\"Panicking!\")panic(1)}defer fmt.Println(\"Defer in g\", i)fmt.Println(\"Printing in g\", i)g(i + 1)}func f() {defer func() {if r := recover(); r != nil {log.Println(\"Recovered in f\", r)}}()fmt.Println(\"Calling g.\")g(0)fmt.Println(\"Returned normally from f.\")}func main() {f()}
四、底层:
这个是我扒出来的底层源码,重点了解heap、link这俩。
type _defer struct {heap bool //表示是分配在堆上还是栈上。rangefunc bool // true for rangefunc listsp uintptr // 栈指针pc uintptr // 程序计数器fn func() // 表示需要被延迟执行的函数。link *_defer // 指向下一个 _defer 结构体的指针。// If rangefunc is true, *head is the head of the atomic linked list// during a range-over-func execution.head *atomic.Pointer[_defer]}
-
defer 语句注册时,会创建一个 _defer 结构体实例。
-
多个 defer 通过 link 字段形成一个单链表(LIFO 栈),挂载到当前 goroutine 的结构上(g._defer)。新的 defer 总是插入链表头部。
主要有两大种分配方式。
1、堆栈分配
区别:分配位置的不同
获取到runtime_defer结构体,它都会被追加到所在 Goroutine _defer 链表的最前面。

2、开放编码
不建额外结构,直接把 defer 代码塞到函数退出前,用位掩码控制执行,开销几乎和普通调用一样。
3、选择:
首先考虑开放编码(已经优化到:实际消耗跟调用普通函数差不多的地步),后栈分配,保底堆分配
以下是整理的Go版本迭代全史,有兴趣的可以一看,挺有趣的。
📅 Go 版本迭代全史(2009–2025)
⭐ 早期阶段
go tool pprof、go vet🔄 每半年发布周期(2013 年起)
go test 支持覆盖率统计sync.Poolsrc/pkg 层级vendor 机制context 包;SSA 后端优化(性能提升 5–35%)defer 性能提升 50%sync.Maperrors.Is/As/Unwrap)defer 性能接近零开销🚀 重大革新阶段(2022–2025)
seq/seq2);gopls 现代化工具链;go get 管理工具链strings/slices/maps 迭代器;增强 WebAssembly 安全性与性能太多了不好记,有兴趣查看时,可以重点看:
1、初始开源-2009-11-10,go降生到了这个世界上
2、Go1.5 :2015,go开始用母语了,实现自举(Go 编译 Go),GC 延迟从 300ms 降至 30ms,奠定现代 Go 基础
3、Go1.11:2018,引入了Go Module解决了依赖问题,让现在的我都收益不止--今年2025
4、Go1.18:2022,泛型、模糊测试、工作区多模块,等均进行了新功能的填充与优化。这个咱暂接触不够多,后期接触了,会回来优化本篇博客
.....


