更多关于延迟函数调用的知识点

延迟调用函数已经在前面介绍过了。限于当时对Go的了解程度,很多延迟调用函数相关的细节和用例并没有之前的文章中提及。这些细节和用例将在本文中列出。

很多有返回值的内置函数是不能被延迟调用的

在Go中,自定义函数的调用的返回结果都可以被舍弃。但是,大多数内置函数(除了copyrecover)的调用的返回结果都不可以舍弃(至少对于标准编译器1.13来说是如此)。另一方面,我们已经了解到延迟函数调用的所有返回结果必须都舍弃掉。所以,很多内置函数是不能被延迟调用的。 幸运的是,在实践中,延迟调用内置函数的需求很少见。根据我的经验,只有append函数有时可能会需要被延迟调用。对于这种情形,我们可以延迟调用一个调用了append函数的匿名函数来满足这个需求。

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := []string{"a", "b", "c", "d"}
  5. defer fmt.Println(s) // [a x y d]
  6. // defer append(s[:1], "x", "y") // 编译错误
  7. defer func() {
  8. _ = append(s[:1], "x", "y")
  9. }()
  10. }

延迟调用的函数值的估值时刻

一个被延迟调用的函数值是在其调用被推入延迟调用堆栈之前被估值的。例如,下面这个例子将输出false

  1. package main
  2. import "fmt"
  3. func main() {
  4. var f = func () {
  5. fmt.Println(false)
  6. }
  7. defer f()
  8. f = func () {
  9. fmt.Println(true)
  10. }
  11. }

一个被延迟调用的函数值可能是一个nil函数值。这种情形将导致一个恐慌。对于这种情形,恐慌产生在此延迟调用被执行而不是被推入延迟调用堆栈的时候。一个例子:

  1. package main
  2. import "fmt"
  3. func main() {
  4. defer fmt.Println("此行可以被执行到")
  5. var f func() // f == nil
  6. defer f() // 将产生一个恐慌
  7. fmt.Println("此行也可以被执行到")
  8. f = func() {} // 此行不会阻止恐慌产生
  9. }

一个延迟调用的实参也是在此调用被推入延迟调用堆栈之前估值的

延迟调用使得代码更简洁和鲁棒

一个例子:

  1. import "os"
  2. func withoutDefers(filepath string, head, body []byte) error {
  3. f, err := os.Open(filepath)
  4. if err != nil {
  5. return err
  6. }
  7. _, err = f.Seek(16, 0)
  8. if err != nil {
  9. f.Close()
  10. return err
  11. }
  12. _, err = f.Write(head)
  13. if err != nil {
  14. f.Close()
  15. return err
  16. }
  17. _, err = f.Write(body)
  18. if err != nil {
  19. f.Close()
  20. return err
  21. }
  22. err = f.Sync()
  23. f.Close()
  24. return err
  25. }
  26. func withDefers(filepath string, head, body []byte) error {
  27. f, err := os.Open(filepath)
  28. if err != nil {
  29. return err
  30. }
  31. defer f.Close()
  32. _, err = f.Seek(16, 0)
  33. if err != nil {
  34. return err
  35. }
  36. _, err = f.Write(head)
  37. if err != nil {
  38. return err
  39. }
  40. _, err = f.Write(body)
  41. if err != nil {
  42. return err
  43. }
  44. return f.Sync()
  45. }
!--https://joeshaw.org/dont-defer-close-on-writable-files/--

上面哪个函数看上去更简洁?显然,第二个使用了延迟调用的函数,虽然只是简洁了些许。另外第二个函数将导致更少的bug,因为第一个函数中含有太多的f.Close()调用,从而有较高的几率漏掉其中一个。 下面是另外一个延迟调用使得代码更鲁棒的例子。如果doSomething函数产生恐慌一个恐慌,则函数f2在退出时将导致互斥锁未解锁。所以函数f1更鲁棒。

  1. var m sync.Mutex
  2. func f1() {
  3. m.Lock()
  4. defer m.Unlock()
  5. doSomething()
  6. }
  7. func f2() {
  8. m.Lock()
  9. doSomething()
  10. m.Unlock()
  11. }

延迟调用可能会导致性能损失

延迟调用并非没有缺点。对于低于1.13版本的官方标准编译器来说,延迟调用将导致一些性能损失。从Go SDK 1.13版本开始,官方标准编译器被一些简单的延迟调用场景做了优化。Go SDK 1.14将会做出更进一步的优化。

延迟调用导致的暂时性内存泄露

一个较大的延迟调用堆栈可能会消耗很多内存,而且延迟调用堆栈中尚未执行的延迟调用可能会导致某些资源未被及时释放。比如,如果下面的例子中的函数需要处理大量的文件,则在此函数推出之前,将有大量的文件句柄得不到释放。

  1. func writeManyFiles(files []File) error {
  2. for _, file := range files {
  3. f, err := os.Open(file.path)
  4. if err != nil {
  5. return err
  6. }
  7. defer f.Close()
  8. _, err = f.WriteString(file.content)
  9. if err != nil {
  10. return err
  11. }
  12. err = f.Sync()
  13. if err != nil {
  14. return err
  15. }
  16. }
  17. return nil
  18. }

对于这种情形,我们应该使用一个匿名函数将需要及时执行延迟的调用包裹起来。比如,上面的函数可以改进为如下:

  1. func writeManyFiles(files []File) error {
  2. for _, file := range files {
  3. if err := func() error {
  4. f, err := os.Open(file.path)
  5. if err != nil {
  6. return err
  7. }
  8. defer f.Close() // 将在此循环步步尾执行
  9. _, err = f.WriteString(file.content)
  10. if err != nil {
  11. return err
  12. }
  13. return f.Sync()
  14. }(); err != nil {
  15. return err
  16. }
  17. }
  18. return nil
  19. }

Go语言101项目目前同时托管在GithubGitlab上。欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏