Golang中Defer的实现及妙用

Go的 defer 语句用于预设一个函数调用(即 推迟执行 函数), 该函数会在执行 defer 的函数返回之前立即执行。

它显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁互斥和关闭文件。

// Contents 将文件的内容作为字符串返回。
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close 会在我们结束后运行。

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append 将在后面讨论。
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // 我们在这里返回后,f 就会被关闭。
        }
    }
    return string(result), nil // 我们在这里返回后,f 就会被关闭。
}

推迟诸如 Close 之类的函数调用有两点好处:第一, 它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时, 这种情况往往就会发生。第二,它意味着“关闭”离“打开”很近, 这总比将它放在函数结尾处要清晰明了。

被推迟函数的实参(如果该函数为方法则还包括接收者)在 推迟 执行时就会求值, 而不是在 调用 执行时才求值。

这样不仅无需担心变量值在函数执行时被改变, 同时还意味着单个已推迟的调用可推迟多个函数的执行。下面是个简单的例子。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

被推迟的函数按照后进先出(LIFO)的顺序执行,因此以上代码在函数返回时会打印 4 3 2 1 0。

一个更具实际意义的例子是通过一种简单的方法, 用程序来跟踪函数的执行。我们可以编写一对简单的跟踪例程:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// 像这样使用它们:
func a() {
    trace("a")
    defer untrace("a")
    // 做一些事情....
}

我们可以充分利用这个特点,即被推迟函数的实参在 defer 执行时才会被求值。 跟踪例程可针对反跟踪例程设置实参。以下例子:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

会打印:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯其它语言中块级资源管理的程序员,defer 似乎有点怪异, 但它最有趣而强大的应用恰恰来自于其基于函数而非块的特点。在 panic 和 recover 这两节中,我们将看到关于它可能性的其它例子。


参考文章:

  • Effective Go: https://golang.org/doc/effective_go.html#defer

你可能感兴趣的:(golang)