乐趣区

关于golang:译更多关于延迟函数

调用带有返回后果的内置函数不能被提早调用

在 Go 中,自定义函数的返回值能够被抛弃。然而,对于有返回值的内置函数,返回后果是不能被抛弃的(起码对于 1.17 版本的 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")
    }()}
提早函数求值

将提早函数(值)被推入以后协程提早调用栈时值就被评估。例如上面打印 false:

package main

import "fmt"

func main() {var f = func () {fmt.Println(false)
    }
    defer f()
    f = func () {fmt.Println(true)
    }
}

提早调用函数的值可能是 nil, 在这种情景下,在被推入到协程提早调用栈中时,nil 函数被调用便会异样,例如:

package main

import "fmt"

func main() {defer fmt.Println("reachable 1")
    var f func() // f is nil by default
    defer f()    // panic here
    // The following lines are also reachable.
    fmt.Println("reachable 2")
    f = func() {} // useless to avoid panicking
}
提早函数接管参数求值

就像下面例子,带参数的提早函数求值也是在推入到以后协程提早函数栈之前。

办法接管参数也不例外。例如上面示例返回 1312:

package main

type T int

func (t T) M(n int) T {print(n)
  return t
}

func main() {
    var t T
    // "t.M(1)" is the receiver argument of the method
    // call ".M(2)", so it is evaluated before the
    // ".M(2)" call is pushed into deferred call stack.
    defer t.M(1).M(2)
    t.M(3).M(4)
}
提早调用使代码更清晰,不容易出错
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()}

哪个看起来更思路清晰?显然带提早调用的,尽管只是一点。对于有很多 f.Close()的函数调用且不应用提早调用极容易漏掉其中一个。

上面的例子展现了提早调用能够缩小很多谬误。如果 doSomething 产生异样,函数 f2 将在锁没有开释的状况下退出。所以 f1 将不会呈现这样的状况:

var m sync.Mutex

func f1() {m.Lock()
    defer m.Unlock()
    doSomething()}

func f2() {m.Lock()
    doSomething()
    m.Unlock()}
提早调用会造成性能损失

应用提早函数调用并不总是好的。在 Go1.13 之前,提早调用是有一些损失。从 1.13 开始,许多提早调用的提案曾经失去了极大的优化,所以个别状况下,咱们没必要放心提早调用带来的性能损失。感激 Dan Scales 做出的优化。

提早调用导致资源透露

一个很大的提早调用栈会占用很大内存,而且一些异样的异样调用也会造成很多资源不能及时开释。例如:在上面函数中许多文件资源待处理,会有大量的文件解决句柄在期待函数退出后开释:

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}
            // The close method will be called at
            // the end of the current loop step.
            defer f.Close()

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

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

    return nil
}

浏览原文

退出移动版