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

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

    在Go中,自定义函数的调用的返回结果都可以被舍弃。 但是,大多数内置函数(除了copy和recover)的调用的返回结果都不可以舍弃(至少对于标准编译器1.17来说是如此)。 另一方面,我们已经了解到延迟函数调用的所有返回结果必须都舍弃掉。 所以,很多内置函数是不能被延迟调用的。

    幸运的是,在实践中,延迟调用内置函数的需求很少见。 根据我的经验,只有函数有时可能会需要被延迟调用。 对于这种情形,我们可以延迟调用一个调用了append函数的匿名函数来满足这个需求。

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

    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函数值。这种情形将导致一个恐慌。 对于这种情形,恐慌产生在此延迟调用被执行而不是被推入延迟调用堆栈的时候。 一个例子:

    前面的文章曾经解释过:一个延迟调用的实参。 方法的属主实参也不例外。比如,下面这个程序将打印出1342

    1. package main
    2. type T int
    3. func (t T) M(n int) T {
    4. print(n)
    5. return t
    6. func main() {
    7. var t T
    8. // t.M(1)是方法调用M(2)的属主实参,因此它
    9. // 将在M(2)调用被推入延迟调用堆栈之前被估值。
    10. defer t.M(1).M(2)
    11. t.M(3).M(4)
    12. }

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

    一个例子:

    下面是另外一个延迟调用使得代码更鲁棒的例子。 如果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. }

    延迟调用并非没有缺点。对于早于1.13版本的官方标准编译器来说,延迟调用将导致一些性能损失。 从Go官方工具链1.13版本开始,官方标准编译器对一些常见的延迟调用场景做了很大的优化。 因此,一般我们不必太在意延迟调用导致的性能损失。感谢Dan Scales实现了此优化。

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

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

    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. }