【问题标题】:Proper way to release resources with defer in a loop?在循环中延迟释放资源的正确方法?
【发布时间】:2022-10-18 07:59:24
【问题描述】:

我需要在循环中对数据库进行 SQL 查询:

for rows.Next() {

   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }
   defer fields.Close()

   // do something with `fields`

}

什么会更好:保持原样或在循环后移动defer

for rows.Next() {

   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }

   // do something with `fields`
}

defer fields.Close()

或者是其他东西 ?

【问题讨论】:

    标签: loops go deferred-execution


    【解决方案1】:

    延迟函数的执行不仅被延迟,延迟到周围函数返回的那一刻,即使封闭函数突然终止,它也会执行,例如恐慌。 Spec: Defer statements:

    “延迟”语句调用一个函数,该函数的执行被推迟到周围函数返回的那一刻,或者因为周围函数执行了return statement,到达了它的function body的末尾,或者因为对应的goroutine是panicking.

    每当您创建一个提供正确关闭/处置它的方法的值或资源时,您应该始终使用 defer 语句来确保它被释放,即使您的其他代码发生恐慌以防止内存或其他系统资源泄漏。

    确实,如果你在循环中分配资源,你不应该简单地使用defer,因为那样就不会释放资源尽早应该(在每次迭代结束时),仅在 for 语句之后(仅在所有迭代之后)。

    你应该做的是,如果你有一个分配此类资源的 sn-p,将其包装在一个函数中——匿名函数或命名函数——,在该函数中你可以使用defer,资源将被释放为一旦不再需要它们,重要的是即使您的代码中存在可能会恐慌的错误。

    例子:

    for rows.Next() {
        func() {
            fields, err := db.Query(...)
            if err != nil {
                // Handle error and return
                return
            }
            defer fields.Close()
    
            // do something with `fields`
        }()
    }
    

    或者如果放入命名函数:

    func foo(rs *db.Rows) {
        fields, err := db.Query(...)
        if err != nil {
            // Handle error and return
            return
        }
        defer fields.Close()
    
        // do something with `fields`
    }
    

    并称它为:

    for rows.Next() {
        foo(rs)
    }
    

    此外,如果您想在第一个错误时终止,您可以从 foo() 返回错误:

    func foo(rs *db.Rows) error {
        fields, err := db.Query(...)
        if err != nil {
            return fmt.Errorf("db.Query error: %w", err)
        }
        defer fields.Close()
    
        // do something with `fields`
        return nil
    }
    

    并称它为:

    for rows.Next() {
        if err := foo(rs); err != nil {
            // Handle error and return
            return
        }
    }
    

    还要注意 Rows.Close() 返回一个错误,当使用 defer 调用时该错误被丢弃。如果我们想检查返回的错误,我们可以使用这样的匿名函数:

    func foo(rs *db.Rows) (err error) {
        fields, err := db.Query(...)
        if err != nil {
            return fmt.Errorf("db.Query error: %w", err)
        }
        defer func() {
            if err = fields.Close(); err != nil {
                err = fmt.Errorf("Rows.Close() error: %w", err)
            }
        }()
    
        // do something with `fields`
        return nil
    }
    

    【讨论】:

    【解决方案2】:

    defer 的全部要点是它在函数返回之前不会执行,因此放置它的合适位置应该是在您要关闭的资源打开之后立即执行。但是,由于您是在循环内创建资源,因此根本不应该使用 defer - 否则,在函数退出之前,您不会关闭在循环内创建的任何资源,因此它们会堆积起来直到然后。相反,您应该在每次循环迭代结束时关闭它们,没有defer:

    for rows.Next() {
    
       fields, err := db.Query(.....)
       if err != nil {
          // ...
       }
    
       // do something with `fields`
    
       fields.Close()
    }
    

    【讨论】:

    • 除此之外,在这种情况下,defer 甚至不会像 OP 预期的那样工作,因为它只会关闭循环中的最后一个 fields(它需要关闭才能正常工作)。顺便说一句,用 defer 将循环内部主体包裹在匿名 func 中可能是一个很好的解决方案。
    • 是的 - 但即使关闭工作正确地, 还是不行出色地.
    • 这是另一种方式。如果您将闭包用于延迟,则只会调用最后一个。对于defer fields.Close(),每次调用都会正确指向不同的指针,当然,它仍然是错误的,因为一旦 func 完成,所有调用都会被调用。
    • 这是否意味着如果您在循环的每次迭代中分配多个资源,并且发生错误,并且您在 if 子句中恐慌而没有先关闭每个打开的资源,那么在上一次迭代期间分配的资源将无法正确关闭? IE。在 for 循环中,不能依赖自动资源清理,而是必须手动清理在此循环迭代中分配的所有资源,以防出现错误?
    • 如果您不延迟关闭并且在恢复时没有关闭资源就恢复了恐慌,是的,您可能会泄漏资源,而不管其他任何事情。恐慌应该是罕见的,通常应该是致命的。如果你要恢复恐慌,你应该敏锐地意识到影响。
    【解决方案3】:

    你可以构造一个局部函数来解决这个问题

        for i := 0; i < 5; i++ {
            func() {
                f, err := os.Open("/path/to/file")
                if err != nil {
                    log.Fatal(err)
                } else {
                    defer f.Close()
                }
            }()
        }
    

    【讨论】:

    • 延迟不应该在错误检查之前吗?
    • 否。如果打开文件时发生错误,则不需要关闭文件。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-19
    • 2014-10-21
    • 2012-10-03
    • 2016-02-03
    • 1970-01-01
    • 2023-03-21
    • 1970-01-01
    相关资源
    最近更新 更多