Go语言中defer按后进先出(LIFO)顺序执行,因每个defer语句在执行时即压入函数私有栈,函数返回时从栈顶依次弹出执行;参数在defer注册时求值,非执行时取值。

多个 defer 为什么不是按代码顺序执行
Go 的 defer 不是“写在前面就先执行”,而是压入函数私有栈,函数返回时统一从栈顶弹出——也就是后进先出(LIFO)。你看到 defer f1() 写在上面、defer f2() 写在下面,实际执行顺序是 f2() 先于 f1()。
常见错误现象:os.Open 后紧接着 bufio.NewReader,然后按“先开后关”直觉写成:
defer file.Close() defer reader.Close()
结果 reader.Close() 先执行,但 reader 依赖 file,file 已关导致 panic:“invalid argument” 或 “use of closed file”。
- 执行顺序只取决于
defer语句在源码中**出现的先后位置**,与缩进、是否在if或for块内无关 - 只要该
defer语句被执行到(没被提前return或panic跳过),就会入栈 -
defer func(){}()是立即调用,不是延迟;真正延迟的是defer func(){}()中的func(){},但加了括号就变成立即执行表达式
defer 参数在注册时就求值,不是执行时
defer fmt.Println(i) 中的 i 是这条 defer 语句执行那一刻的值快照,不是函数退出时的值。循环里直接这么写,几乎必然出错。
立即学习“go语言免费学习笔记(深入)”;
典型错误场景:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
输出是 2、1、0(LIFO + 快照),而不是你以为的 0、1、2 或三个 2。
- 值类型(如
int、string)拷贝的是当时值 - 指针或结构体字段若被后续修改,
defer执行时看到的是修改后的状态(因为地址没变) - 要捕获当前循环变量值,得用闭包传参:
defer func(x int) { fmt.Println(x) }(i)
资源释放必须严格遵循“后开先关”逆序
嵌套资源(文件→缓冲读取器、数据库连接→事务、锁→goroutine)的关闭顺序,必须和创建顺序相反。这不是风格问题,是避免 invalid state 的硬性要求。
例如:
- 先
db.Begin(),再tx.Query()→defer tx.Rollback()必须在defer db.Close()之前注册 - 先
mu.Lock(),再启 goroutine 处理 →defer mu.Unlock()必须确保 goroutine 已结束,否则可能 race;更稳妥是把 unlock 放在 goroutine 内部或用sync.Once - HTTP handler 中
defer resp.Body.Close()要放在http.Get之后立刻注册,不能等所有逻辑跑完才写,否则中间 panic 会导致泄露
panic 时 defer 仍执行,recover 必须在 defer 内
defer 在 panic 发生后依然会执行,这是 Go 错误恢复机制的基础。但 recover() 只有在 defer 函数内部调用才有效。
错误写法:
func f() {
recover() // 这个永远不生效
panic("boom")
}
正确写法:
func f() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered: %v", r)
}
}()
panic("boom")
}
-
recover()只对同 goroutine 中、且尚未传播出去的 panic 有效 - 多个
defer都含recover()时,只有最靠近 panic 的那个(即最后注册的)能捕获,其余看到的是已 recover 状态 - 不要在 defer 外层 try-catch 式包裹 recover,它只在 defer 函数体内有意义

















