defer 的作用用于延迟处理,在golang中,它一般在方法即将结束返回时调用, 因此,我们使用defer最多的场景就是对资源的释放,这种资源包括连接资源(http连接,数据库连接等),文件资源(file相关),锁资源(sync.mutex), 对资源的释放处理不受panic的影响,意思就是方法体内即使有panic,也阻止不了defer的执行。除了对资源的释放外,defer的第二大用处就是用于捕获异常。当然,常规的使用这些,都不是我们这篇文章的重点。
在引言中, 提到了两个defer最常用的使用场景, 虽然不是这篇文章的重点,但可以简单的回顾一下。
defer的常用使用场景
释放资源 - 释放锁资源
func DoSomething(mu *sync.Mutex, s []string) {
mu.Lock()
defer mu.Unlock()
s = append(s, "test")
}
释放资源 - 释放数据库连接资源
func fetchDataFromDB() error {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
return err
}
defer db.Close() // 确保数据库连接在函数结束时被关闭
// 执行数据库查询操作
// ...
return nil
}
资源释放 - http资源释放
// 最容易遗忘导致内存泄露的场景
resp, err := http.Get("https://example.com")
if err != nil {
// 错误处理
return
}
defer resp.Body.Close() // 确保在函数结束时关闭响应体
// 使用 resp.Body 读取响应
// ...
异常捕获
func dosomething(){
defer func(){
if err := recover(); err != nil {
fmt.Println(err)
}
}
// dosomething
}
基于以上几种常用的使用场景,引出我们今天的主要主题,如何和for循环结合使用。众所周知,defer是在方法返回值的时候执行的,那如果方法中包含for循环, 就得等所有的for循环执行完毕后,显然,这种不太合乎我们的想法,同时也会带来资源得不到及时的释放,导致内存泄漏或者资源空间占用太多的情况。因此,我们的目标是如何在for循环的每一层执行结束,就把资源及时释放掉。
defer在for循环中的应用
显然,我们需要在for循环的每一层构建一个方法,而把defer放置在里边。这样每一层结束之后,就可以释放掉对应的资源了。
所以问题的解决方案变成了“如何构建一个方法”, 原来铺垫这么久,就是为了引出这个?
- 最简单的构建方法 - 匿名函数 一个小例子:在for循环中释放file资源
|
|
- 优雅的定义方法 - 直接定义个方法调用
匿名函数简单,但同时也要正视其缺点,可读性差,且无法复用,所以我们可以将部分逻辑提取出来,独立成单独的方法。
func ReadFile(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
err = processFile(file)
if err != nil {
return err
}
}
return nil
}
func processFile(file *os.File) error {
defer func() {
file.Close()
fmt.Printf("close %s\n", file.Name())
}()
content, err := io.ReadAll(file)
if err != nil {
return err
}
fmt.Printf("%s content: %s\n", file.Name(), content)
return nil
}
对于常用的需要复用的方法,我们可以提前去约定个方法处理, 比如,处理锁资源的释放
func withLocker(mu sync.Locker, fn func() error) error {
mu.Lock()
defer mu.UnLocker()
return fn()
}
这样,如果以后在for循环中需要用到锁资源的释放的话,就可以直接调用。
参考文档:
https://www.zhihu.com/tardis/zm/art/428343854?source_id=1003