go从零单排之defer

0 阅读6分钟

时间因为回忆变得厚重

image.png

一、Go defer 白话核心:“先记下来,最后再执行”

defer 是 Go 里的一个关键字,作用是把紧跟它的函数调用 “登记” 到当前函数的 “延迟执行列表” 里,等当前函数执行完(return / 报错 / 正常结束)前,再按「后进先出」的顺序执行这些登记的函数。

大白话类比:你在收拾书桌(当前函数),过程中想到 “用完剪刀要归位”“喝完水杯要洗”“离开前要关灯”,就把这些事挨个记在便签上(defer 登记),等书桌收拾完(当前函数结束),再从最后记的事开始做(先关灯→洗水杯→归位剪刀)。


二、defer 基础用法(先看最简单的示例)

示例 1:基础执行顺序

package main
import "fmt"
func main() {
    fmt.Println("开始执行main函数")
    
    // 登记第一个defer函数
    defer fmt.Println("执行defer 1:归位剪刀")
    // 登记第二个defer函数
    defer fmt.Println("执行defer 2:洗水杯")
    // 登记第三个defer函数
    defer fmt.Println("执行defer 3:关灯")
    
    fmt.Println("正在收拾书桌...")
    // main函数执行完,开始执行defer列表
}

输出结果

开始执行main函数
正在收拾书桌...
执行defer 3:关灯
执行defer 2:洗水杯
执行defer 1:归位剪刀

核心规律

  • defer 函数「登记时不执行」,只记下来;
  • 执行顺序是「后进先出(LIFO)」—— 最后登记的最先执行。

示例 2:defer 带参数(参数在登记时就确定值)

package main
import "fmt"
func main() {
    num := 10
    
    // 登记时,参数num的值已经确定是10(不是执行时的20)
    defer fmt.Println("defer执行:num =", num)
    
    num = 20
    fmt.Println("main执行:num =", num)
}

输出结果

main执行:num = 20
defer执行:num = 10

关键坑点:defer 函数的参数在「登记时」就计算出值并保存,执行时不会再读最新值。

示例 3:defer 修饰函数(不是直接写语句)

package main
import "fmt"
// 定义一个普通函数
func sayHi(name string) {
    fmt.Printf("Hi, %s\n", name)
}
func main() {
    // defer 可以修饰函数调用(参数同样在登记时确定)
    defer sayHi("张三")
    defer sayHi("李四")
    
    fmt.Println("main函数执行中")
}

输出结果

main函数执行中
Hi, 李四
Hi, 张三

三、defer 核心特性(白话拆解)

1. 执行时机:当前函数 “退出前”(3 种场景)

  • 场景 1:函数正常执行完 return 后;
  • 场景 2:函数发生 panic(崩溃)时;
  • 场景 3:函数被手动调用 runtime.Goexit() 退出时。

示例:panic 时 defer 仍执行

package main
import "fmt"
func main() {
    defer fmt.Println("defer:程序崩溃了也会执行我")
    
    fmt.Println("main执行中...")
    // 手动触发panic
    panic("发生错误!")
    fmt.Println("这行不会执行") // panic后函数直接退出,这行跳过
}

输出结果

main执行中...
defer:程序崩溃了也会执行我
panic: 发生错误!
...(后续panic堆栈信息)

2. defer 可以修改函数返回值(关键!)

如果 defer 函数是「闭包」(引用外部变量),能修改函数的返回值(因为返回值在 defer 执行前已经分配内存)。

示例 1:修改命名返回值

package main
import "fmt"
// 命名返回值:res 是提前定义的返回变量
func calc() (res int) {
    defer func() {
        res += 10 // defer 闭包修改返回值res
    }()
    
    res = 5 // 先赋值5
    return  // 返回时,先执行defer,再返回res
}
func main() {
    fmt.Println(calc()) // 输出 15(5+10)
}

示例 2:不修改匿名返回值(对比)

package main
import "fmt"
// 匿名返回值:返回值没有提前命名
func calc() int {
    num := 5
    defer func() {
        num += 10 // 修改的是局部变量num,不是返回值
    }()
    
    return num // 返回值在return时已经确定是5,defer改num没用
}
func main() {
    fmt.Println(calc()) // 输出 5
}

核心结论:只有「命名返回值」能被 defer 闭包修改,匿名返回值不行。

3. defer 释放资源(最常用的业务场景)

这是 defer 最核心的实际用途:打开资源(文件 / 连接 / 锁)后,立刻用 defer 登记 “关闭资源”,避免忘记释放。

示例 1:关闭文件

package main
import (
    "fmt"
    "os"
)
func readFile(filename string) {
    // 打开文件
    file, err := os.Open(filename)
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    
    // 登记关闭文件(不管函数怎么退出,都会执行)
    defer func() {
        err := file.Close()
        if err != nil {
            fmt.Println("关闭文件失败:", err)
        } else {
            fmt.Println("文件已关闭")
        }
    }()
    
    // 读取文件(模拟业务逻辑)
    fmt.Println("正在读取文件内容...")
}
func main() {
    readFile("test.txt")
}

示例 2:释放锁

package main
import (
    "fmt"
    "sync"
)
var mutex sync.Mutex
var count int
func add() {
    // 加锁
    mutex.Lock()
    // 登记解锁(避免忘记解锁导致死锁)
    defer mutex.Unlock()
    
    count++
    fmt.Println("count =", count)
}
func main() {
    add()
    add()
}

示例 3:关闭数据库连接

package main
import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)
func queryDB() {
    // 连接数据库
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
    if err != nil {
        fmt.Println("连接数据库失败:", err)
        return
    }
    // 登记关闭连接
    defer db.Close()
    
    // 执行查询(模拟业务)
    fmt.Println("执行数据库查询...")
}
func main() {
    queryDB()
}

四、defer 常见坑点(避坑示例)

坑点 1:循环中使用 defer 导致资源泄漏

package main
import (
    "fmt"
    "os"
)
// 错误写法:循环里的defer会等到函数结束才执行,导致同时打开100个文件
func badExample() {
    for i := 0; i < 100; i++ {
        file, err := os.Open(fmt.Sprintf("file%d.txt", i))
        if err != nil {
            continue
        }
        defer file.Close() // 登记100个close,但都等函数结束才执行
        // 读取文件...
    }
}
// 正确写法:把循环体逻辑抽成子函数,defer在子函数里执行
func goodExample() {
    for i := 0; i < 100; i++ {
        // 子函数执行完就会关闭文件
        func(num int) {
            file, err := os.Open(fmt.Sprintf("file%d.txt", num))
            if err != nil {
                return
            }
            defer file.Close()
            // 读取文件...
        }(i)
    }
}
func main() {
    goodExample()
}

坑点 2:defer 执行顺序与 return 混用

package main
import "fmt"
func foo() int {
    defer fmt.Println("defer执行")
    return 10
}
func main() {
    res := foo()
    fmt.Println("main拿到结果:", res)
}

输出结果

defer执行
main拿到结果: 10

执行流程:return 10 → 先把返回值 10 赋值给临时变量 → 执行 defer → 把临时变量返回给 main。


五、defer 典型业务场景总结

  1. 资源释放:文件 / 数据库连接 / 网络连接 / 锁的关闭 / 释放(最核心场景);
  1. 异常恢复:配合 recover() 捕获 panic,避免程序崩溃;
  1. 日志记录:函数退出前记录执行时间、结果等日志;
  1. 清理临时数据:函数执行完删除临时文件、清空缓存等。

示例:defer + recover 捕获 panic

package main
import "fmt"
func riskyFunc() {
    // 登记recover,捕获panic
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获到错误:", err)
        }
    }()
    
    // 触发panic
    panic("数组越界")
}
func main() {
    riskyFunc()
    fmt.Println("程序继续执行,没有崩溃")
}

输出结果

捕获到错误: 数组越界
程序继续执行,没有崩溃

总结

  1. defer 核心:登记函数延迟执行,执行时机是当前函数退出前,顺序是「后进先出」;
  1. 关键特性:参数在登记时确定值,可修改命名返回值,panic 时仍会执行;
  1. 核心用途:释放资源(文件 / 连接 / 锁)、捕获 panic、日志记录,是 Go 工程化开发的必备技巧;
  1. 避坑要点:循环中避免直接用 defer,区分命名 / 匿名返回值的修改规则。

简单记:defer 就是 “先记后做,倒序执行,兜底释放”,只要涉及 “打开 - 关闭” 类操作,第一时间加 defer 准没错。