调用带有返回后果的内置函数不能被提早调用
在Go中,自定义函数的返回值能够被抛弃。然而,对于有返回值的内置函数,返回后果是不能被抛弃的(起码对于1.17版本的Go编译器是这样的),除了内置Copy和Recover函数是例外。另一方面,咱们晓得,提早函数的返回值必须抛弃,所以很多内置函数不能当作提早函数应用。
侥幸的是,内置函数(带有返回值)在实践中很少应用。据我所知,只有Append函数有时候在提早中调用。这种状况,咱们能够将append包装到匿名函数中调用。
package mainimport "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 mainimport "fmt"func main() { var f = func () { fmt.Println(false) } defer f() f = func () { fmt.Println(true) }}
提早调用函数的值可能是nil,在这种情景下,在被推入到协程提早调用栈中时,nil函数被调用便会异样,例如:
package mainimport "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 maintype T intfunc (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.Mutexfunc 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}
浏览原文