Go 延迟调用

2019/12/11 Go

语句 defer 向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定以及错误处理等操作。

延迟调用,注册的是调用,参数在注册时被复制并缓存起来。多个延迟注册按 FILO 的次序执行。

func main() {
    x, y := 1, 2

    defer func(i int) {
        fmt.Println("defer x, y =", i, y)
    }(x)

    defer func() {
        x += 1
        y += 1
    }()

    x += 1
    y += 1
    fmt.Println(x, y)
}

// 输出:
//  2 3
//  defer x, y = 1 4

对延迟调用的不合理使用会浪费更多资源,甚至造成逻辑错误。如下,不恰当的 defer 导致文件关闭时间延长。

func main() {
    for i := 0; i < 10000; i ++ {
        path := fmt.SPrintf(".log/%d.txt", i)
        
        f, err := os.Open(path)
        if err != nil {
            log.Println(err)
            continue
        }
        
        // 在 main 结束时才会执行,延长了逻辑结束时间和 f 的生命周期,消耗更多的内存等资源
        defer f.Close()
    }
}

应该直接调用,或者重构为函数,将循环和处理算法分离。

func main() {
    do := func(n int) {
        path := fmt.SPrintf(".log/%d.txt", i)
        
        f, err := os.Open(path)
        if err != nil {
            log.Println(err)
            continue
        }
        
        // 在该匿名函数结束时执行,而不是 main
        defer f.Close()
    }

    for i := 0; i < 10000; i ++ {
        do(i)
    }
}

性能

相比直接用 CALL 汇编指令调用函数,延迟调用需要花费更多代价。这其中包括注册、调用等操作,还有额外的缓存开销。

// deferBenchmark/deferBenchmark_test.go
package deferBenchmark

import (
    "sync"
    "testing"
)

var m sync.Mutex

func call() {
    m.Lock()
    m.Unlock()
}

func deferCall() {
    m.Lock() 
    defer m.Unlock() 
}

func BenchmarkCall(b *testing.B) {
    for i := 0; i < b.N; i ++ {
        call()
    }
}

func BenchmarkDefer(b *testing.B) {
    for i := 0; i < b.N; i ++ {
        deferCall()
    }
}

测试以上代码:

$ cd path/to/deferBenchmark
$ go test -bench=.
goos: darwin
goarch: amd64
BenchmarkCall-4   100000000         18.7 ns/op
BenchmarkDefer-4  30000000          57.7 ns/op
PASS
ok    _/var/gonotes/src/github.com/lwlwilliam/deferBenchmark    3.691s

可以看出,延迟调用跟普通调用的速度相差了几倍,因此,对那些性能要求高且压力大的算法,应避免使用延迟调用。

Search

    Table of Contents