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放置在里边。这样每一层结束之后,就可以释放掉对应的资源了。

所以问题的解决方案变成了“如何构建一个方法”,  原来铺垫这么久,就是为了引出这个?

  1. 最简单的构建方法 - 匿名函数 ​一个小例子:在for循环中释放file资源​
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

    func main() {
    	err := ReadFile([]string{"foo.txt", "bar.txt"})
    	fmt.Printf("ReadFile err: %v\n", err)
    }
    
    func ReadFile(paths []string) error {
    	for _, path := range paths {
    		file, err := os.Open(path)
    		if err != nil {
    			return err
    		}
    		err = func() 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
    		}()
    		if err != nil {
    			return err
    		}
    	}
    	return nil
    }
  1. 优雅的定义方法 - 直接定义个方法调用

匿名函数简单,但同时也要正视其缺点,可读性差,且无法复用,所以我们可以将部分逻辑提取出来,独立成单独的方法。

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://jianghushinian.cn/2023/06/23/how-to-implement-a-context-manager-similar-to-python-s-with-in-go/

https://www.zhihu.com/tardis/zm/art/428343854?source_id=1003