关于go 语言中的延迟执行函数

许多内置的带有返回值的函数无法进行延迟调用

在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")
    }()
}

延迟函数值的时刻

延迟函数调用中的被调用函数可以是零函数值。对于这种情况,恐慌将在准备延迟调用被推入当前goroutine的延迟调用栈之前发生。

package main

import "fmt"

func main() {
    defer fmt.Println("reachable")
    var f func() // f is nil by default
    defer f()    // panic here
    // The following lines are dead code.
    fmt.Println("not reachable")
    f = func() {}
}

在将延迟调用推入当前goroutine的延迟调用堆栈之前,还会计算延迟函数调用的参数

延迟调用使得代码清晰,并且bug少

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()
}

延迟函数调用导致的性能损失

使用延迟函数调用并不总是好的。到目前为止(Go 1.12),对于官方Go编译器,延迟函数调用将在运行时导致一些性能损失.
例如,在以下示例中,CounterB和IncreaseB方法比CounterA和IncreaseA方法更有效。

import "sync"

type T struct {
    mu sync.Mutex
    n  int64
}

func (t *T) CounterA() int64 {
    t.mu.Lock()
    defer t.mu.Unlock()
    return t.n
}

func (t *T) CounterB() (count int64) {
    t.mu.Lock()
    count = t.n
    t.mu.Unlock()
    return
}

func (t *T) IncreaseA() {
    t.mu.Lock()
    defer t.mu.Unlock()
    t.n++
}

func (t *T) IncreaseB() {
    t.mu.Lock()
    t.n++ // this line will not panic for sure
    t.mu.Unlock()
}

在B版本的函数中,我们应该保证Lock和Unlock调用之间的代码永远不会出现恐慌。通常,建议在实践中使用A版本功能。当我们真正关心所涉及功能的性能时,我们应该只采用B版本。

延迟函数调用导致的资源泄漏

非常大的延迟调用堆栈也可能消耗大量内存,并且未执行的延迟调用可能会阻止某些资源及时释放。例如,如果在调用以下函数时需要处理许多文件,则在函数退出之前将不会释放大量文件处理程序。

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
            }
            defer f.Close()

            _, err = f.WriteString(file.content)
            if err != nil {
                return err
            }

            return f.Sync()
        }(); err != nil {
            return err
        }
    }

    return nil
}

推荐阅读更多精彩内容

  • 天气变暖,晚饭后出来遛弯的人就多了。 我一向是比较懒,宁可宅在家看书,也不愿意出门。尤其是有了孩子之后,出门都感觉...
    萨巴Q阅读 276评论 2 13
  • 1 月光穿过树梢,撒在老五的身上。老五抬起头,只看到密密麻麻的树叶,和叶子缝隙中的上弦月。 这是一片树林,且树木长...
    浪大虾阅读 161评论 0 7
  • 情人节要到啦~如果是两个人一起甜蜜的度过,可以选择对方和自己都更适合更喜欢的方法~ 如果是自己过,也要给自己准备一...
    西滢筱阅读 35评论 0 0
  • 当我要买电车的时候,母亲说你近视眼,卖电车干什么?骑自行车就行啊。听了妈妈的话,我心里烦的不要不要的。我在...
    春语墨然阅读 82评论 1 3