在循环中使用defer释放资源的正确方法是什么?

huangapple go评论83阅读模式
英文:

Proper way to release resources with defer in a loop?

问题

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

for rows.Next() {

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

   // 对`fields`进行操作

}

哪种方式更好:保持原样还是将defer移到循环后面:

for rows.Next() {

   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }
       
   // 对`fields`进行操作
}

defer fields.Close()

或者其他方式?

英文:

I need to make SQL queries to database in the loop:

for rows.Next() {

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

   // do something with `fields`

}

What will be better: leave all as is or move defer after loop:

for rows.Next() {

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

defer fields.Close()

Or something else ?

答案1

得分: 127

延迟函数的执行不仅仅是延迟到包围函数返回的时候,还会在封闭函数异常终止(如发生 panic)时执行。无论何时你创建一个值或资源,并且该值或资源提供了关闭或释放的方法,你都应该使用 defer 语句来确保它在代码发生 panic 时也能被释放,以防止内存泄漏或其他系统资源泄漏。

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

你应该将分配资源的代码片段封装在一个函数中,可以是匿名函数或命名函数,在该函数中使用 defer,这样资源将在不再需要时立即释放,而且最重要的是,即使代码中有 bug 导致 panic 也能正常释放资源。

示例:

for rows.Next() {
	func() {
		fields, err := db.Query(...)
		if err != nil {
			// 处理错误并返回
			return
		}
		defer fields.Close()

		// 使用 `fields` 做一些操作
	}()
}

或者放在一个命名函数中:

func foo(rs *db.Rows) {
	fields, err := db.Query(...)
	if err != nil {
		// 处理错误并返回
		return
	}
	defer fields.Close()

	// 使用 `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()

	// 使用 `fields` 做一些操作
	return nil
}

调用它:

for rows.Next() {
	if err := foo(rs); err != nil {
		// 处理错误并返回
		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)
		}
	}()

	// 使用 `fields` 做一些操作
	return nil
}
英文:

Execution of a deferred function is not only delayed, deferred to the moment the surrounding function returns, it is also executed even if the enclosing function terminates abruptly, e.g. panics. Spec: Defer statements:

> A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

Whenever you create a value or a resource that provides means to properly close it / dispose of it, you should always use a defer statement to make sure it is released even if your other code panics to prevent leaking memory or other system resources.

It's true that if you're allocating resources in a loop you should not simply use defer, as then releasing resources will not happen as early as it could and should (at the end of each iteration), only after the for statement (only after all iterations).

What you should do is that if you have a snippet that allocates such resources, wrap it in a function –either an anonymous or a named function–, and in that function you may use defer, and resources will be freed as soon as they are no longer needed, and what's important is that even if there is a bug in your code which may panic.

Example:

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

		// do something with `fields`
	}()
}

Or if put in a named function:

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

	// do something with `fields`
}

And calling it:

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

Also if you'd want to terminate on the first error, you could return the error from 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
}

And calling it:

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

Also note that Rows.Close() returns an error which when called using defer is discarded. If we want to check the returned error, we can use an anonymous function like this:

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

得分: 81

defer的整个目的是在函数返回之前不执行,因此应该将其放在打开要关闭的资源之后的适当位置。然而,由于你是在循环内部创建资源,所以根本不应该使用defer,否则直到函数退出之前,你将不会关闭循环内创建的任何资源,它们会一直堆积。相反,你应该在每次循环迭代结束时关闭它们,不使用 defer

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

    // 使用`fields`做一些操作

    fields.Close()
}
英文:

The whole point of defer is that it does not execute until the function returns, so the appropriate place to put it would be immediately after the resource you want to close is opened. However, since you're creating the resource inside the loop, you should not use defer at all - otherwise, you're not going to close any of the resources created inside the loop until the function exits, so they'll pile up until then. Instead, you should close them at the end of each loop iteration, without defer:

for rows.Next() {

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

   // do something with `fields`

   fields.Close()
}

答案3

得分: 4

你可以构建一个本地函数来解决这个问题。

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

请注意,我已经将HTML实体编码转换为正常字符。

英文:

You can construct a local function to solve this problem

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

huangapple
  • 本文由 发表于 2017年8月10日 23:23:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/45617758.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定