调用带有返回结果的内置函数不能被延迟调用
在Go中,自定义函数的返回值可以被丢弃。然而,对于有返回值的内置函数,返回结果是不能被丢弃的(起码对于1.17版本的Go编译器是这样的),除了内置Copy和Recover函数是例外。另一方面,我们知道,延迟函数的返回值必须丢弃,所以很多内置函数不能当作延迟函数使用。
幸运的是,内置函数(带有返回值)在实践中很少使用。据我所知,只有Append函数有时候在延迟中调用。这种情况,我们可以将append包装到匿名函数中调用。
package main
import "fmt"
func main() {
s := []string{"a", "b", "c", "d"}
defer fmt.Println(s) // [a x y d]
// defer append(s[:1], "x", "y") // error
defer func() {
_ = append(s[:1], "x", "y")
}()
}
延迟函数求值
将延迟函数(值)被推入当前协程延迟调用栈时值就被评估。例如下面打印false:
package main
import "fmt"
func main() {
var f = func () {
fmt.Println(false)
}
defer f()
f = func () {
fmt.Println(true)
}
}
延迟调用函数的值可能是nil,在这种情形下,在被推入到协程延迟调用栈中时,nil函数被调用便会异常,例如:
package main
import "fmt"
func main() {
defer fmt.Println("reachable 1")
var f func() // f is nil by default
defer f() // panic here
// The following lines are also reachable.
fmt.Println("reachable 2")
f = func() {} // useless to avoid panicking
}
延迟函数接收参数求值
就像上面例子,带参数的延迟函数求值也是在推入到当前协程延迟函数栈之前。
方法接收参数也不例外。例如下面示例返回1312:
package main
type T int
func (t T) M(n int) T {
print(n)
return t
}
func main() {
var t T
// "t.M(1)" is the receiver argument of the method
// call ".M(2)", so it is evaluated before the
// ".M(2)" call is pushed into deferred call stack.
defer t.M(1).M(2)
t.M(3).M(4)
}
延迟调用使代码更清晰,不容易出错
import "os"
func withoutDefers(filepath string, head, body []byte) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
_, err = f.Seek(16, 0)
if err != nil {
f.Close()
return err
}
_, err = f.Write(head)
if err != nil {
f.Close()
return err
}
_, err = f.Write(body)
if err != nil {
f.Close()
return err
}
err = f.Sync()
f.Close()
return err
}
func withDefers(filepath string, head, body []byte) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
defer f.Close()
_, err = f.Seek(16, 0)
if err != nil {
return err
}
_, err = f.Write(head)
if err != nil {
return err
}
_, err = f.Write(body)
if err != nil {
return err
}
return f.Sync()
}
哪个看起来更思路清晰?显然带延迟调用的,虽然只是一点。对于有很多f.Close()的函数调用且不使用延迟调用极容易漏掉其中一个。
下面的例子展示了延迟调用可以减少很多错误。如果doSomething发生异常,函数f2将在锁没有释放的情况下退出。所以f1将不会出现这样的情况:
var m sync.Mutex
func f1() {
m.Lock()
defer m.Unlock()
doSomething()
}
func f2() {
m.Lock()
doSomething()
m.Unlock()
}
延迟调用会造成性能损失
使用延迟函数调用并不总是好的。在Go1.13之前,延迟调用是有一些损失。从1.13开始,许多延迟调用的提案已经得到了极大的优化,所以一般情况下,我们没必要担心延迟调用带来的性能损失。感谢Dan Scales 做出的优化。
延迟调用导致资源泄漏
一个很大的延迟调用栈会占用很大内存,而且一些异常的异常调用也会造成很多资源不能及时释放。例如:在下面函数中许多文件资源待处理,会有大量的文件处理句柄在等待函数退出后释放:
func writeManyFiles(files []File) error {
for _, file := range files {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
err = f.Sync()
if err != nil {
return err
}
}
return nil
}
对于这种情况,我们可以使用匿名函数来封装延迟调用,以便延迟函数调用更早执行。可以重写为:
func writeManyFiles(files []File) error {
for _, file := range files {
if err := func() error {
f, err := os.Open(file.path)
if err != nil {
return err
}
// The close method will be called at
// the end of the current loop step.
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
return f.Sync()
}(); err != nil {
return err
}
}
return nil
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。