Go常见错误集锦 | 循环内部使用defer的正确姿势

2023-01-31 15:39:58 浏览数 (1)

众所周知,Golang中的defer关键词可以在函数返回前执行一些操作,常用的就是避免死板的代码、释放资源以避免内存泄露。具体的可参考我之前的关于defer文章使用defer释放资源和你知道defer的参数和接收者是如何被取值的吗两篇文章。

本文给大家介绍一些在使用循环语句内部使用defer会遇到的坑以及如何避免。下面是一个在循环中打开一组文件的函数例子。在该函数中,会从一个通道中不断的接收文件路径。然后通过遍历该通道,打开对应路径的文件,然后在使用完毕后关闭该文件资源。代码如下:

代码语言:javascript复制
func readFiles(ch <-chan string) error {
  for path := range ch {
    file, err := os.Open(path)
    if err != nil {
      return err
    }
    defer file.Close()
    // Do something with file
  }
  return nil
}

这段代码会有什么问题吗?我们知道defer的调用是在其所在函数返回的时候才会发生的。在该示例中,defer的调用不是在每次迭代结束,而是readFiles函数返回时。如果readFiles没有返回,被打开的文件标识符就一直保持打开状态,甚至会造成内存泄露。

那应该如何修复该问题呢?我们将原来的逻辑拆分出一个新的函数readFile,将打开文件、延迟关闭文件资源、处理文件的逻辑放到里面。如下:

代码语言:javascript复制
func readFiles(ch <-chan string) error {
  for path := range ch {
    if err := readFile(path); err != nil {
      return err
    }
  }
  return nil
}

func readFile(path string) error {
  file, err := os.Open(path)
  if err != nil {
    return err
  }
  defer file.Close()
  // Do something with file
  return nil
}

这样,当readFile函数返回时,函数中的defer就会被立即调用,也就是在循环的每个迭代完成后就会立即调用。因此,避免了在for循环所在函数还未返回时一直保持着文件标识符打开的状态。

还有另外一种方法就是使用匿名函数,但其本质思想是一样的。如下:

代码语言:javascript复制
func readFiles(ch <-chan string) error {
  for path := range ch {
    err := func() error {
      file, err := os.Open(path)
      if err != nil {
        return err
      }
      defer file.Close()
      // Do something with file
      return nil
    }()

    if err != nil {
      return err
    }
  }
  return nil
}

哪种方式更好一些呢?相比较而言个人觉得还是第一种方式更好,符合面向对象的单一职责原则。即一个函数只干一件事。这样也利于单元测试。

总之,在实际的编程过程中,谨记defer的执行是在其所在函数返回时才执行的这条原则

0 人点赞