Go Defer 需要避免的五个坑(第一部分,共三部分)

1 — 延迟的 nil 函数#

函数内的其他内容执行完成后,defer 被调用时,如果延迟函数的执行结果为 nil, 则会 panics

示例#


func() {

var run func() = nil

defer run() fmt.Println("runs")

}

输出#


runs❗️ panic: runtime error: invalid memory address or nil pointer dereference

为什么?#

这里,函数一直执行到结束,之后延迟函数运行并 panic,因为其执行结果为 nul. 但是 run() 函数可以注册而没有问题,因为他在包含的函数结束之前没有被真正调用。

这是一个简单的例子,但是在现实世界也会发生同样的事情,因此如果你遇到类似的情况,请怀疑可能是因为这个问题而导致的。

2 — 循环内的延迟#

除非你确定自己在做什么,否则不要在循环那使用 defer, 因为它可能不会像预期的那样工作。

但是,有时在循环中使用它可能会变的很方便,例如,将函数的递归性委托给 defer, 但这不在本文的讨论范围.

Go

这里,循环内调用的 defer row.Close() 在整个函数结束之前不会执行 - 不是在 for 循环的每次循环结束时执行。此处的所有调用都将占用函数的堆栈,并可能导致无法预见的问题。

解决方案 #1:#

直接调用,不使用 defer.

Go

解决方案 #2:#

将工作委托给另一个函数,并在那个函数中使用 defer。在这里,每次匿名函数结束后,延迟函数将执行。

Go

3 — Defer 作为包装器#

有时候你需要延迟使用闭包,以使其更加实用,或者一些其他的原因我现在还无法猜到。例如,为了打开一个数据库连接,然后运行一些查询并在最后保证它断开了连接。

例如#


type database struct{}

func (db *database) connect() (disconnect func()) {
    fmt.Println("connect")
    return func() {
        fmt.Println("disconnect")
    }
}

运行#


db := &database{}

defer db.connect()

fmt.Println("query db...")

输出#


query db...

connect

为什么它没有生效?#

它没有断开连接,而是在最后连接,这是一个 bug。这里发生的唯一一件事是 connect() 被保存到这个函数结束也没有运行。

解决#


func() {
    db := &database{} 
    close := db.connect()
    defer close()
    fmt.Println("query db...")
}

现在, db.connect() 返回一个函数,当周围的函数执行完毕后,我们可以用它延迟断开与数据库的连接。

输出#


connect

query db...

disconnect

不好的做法:#

尽管这样做很不好,但是我想给你展示的是如何在没有变量的情况下去断开与数据库的连接。因此,我希望你能了解到 defer 是如何工作的,以及更一般的情况下 Go 是如何工作的。


func() {
    db := &database{} 
    defer db.connect()() ..
}

这段代码在技术层面与上面的解决方案几乎相同。这里,第一个括号用于连接到数据库 (立即发生在 `defer db.connect()`),然后第二个括号用于在周围的函数执行结束后延迟执行断开连接的函数 (返回的闭包)。

这是因为 db.connect() 创建了一个闭包值,然后延迟注册。db.connect() 的值需要先解析才能注册到 defer。这与 defer 没有直接关系,但是它可能会解决您可能遇到的一些问题。

4 — 块中的延迟#

你可能希望延迟的函数会在块结束后运行,但它不会,它只会在包含的函数结束后执行。这也适用于所有块:For,switch 等等。除了我们在前面的技巧中看到的函数块。

因为:defer 属于函数而不是块。

例如#


func  main() {

    {

        defer  func() {

            fmt.Println("block: defer runs")

        }()

        fmt.Println("block: ends")

    }

    fmt.Println("main: ends")

}

输出#


block: ends

main: ends

block: defer runs

解释#

上面的延迟函数仅在函数结束时运行,而不是延迟函数的周围块结束 (包含延迟调用的花括号内的区域
)。如示例代码所示,您可以使用大括号创建单独的块。

另一种解决方案#

如果您想在块中运行 defer,您可以将其转换为函数,例如匿名函数,就像 gotcha #2 的解决方案一样。


func main() {

func() {

defer func() {

fmt.Println("func: defer runs")

}()

fmt.Println("func: ends")

}()

fmt.Println("main: ends")

}

5 — 延迟方法#

你还可以这样使用 defer 方法. 但是,这样会有一个怪癖,看。

无指针#


type Car struct {

model string

}

func (c Car) PrintModel() {

fmt.Println(c.model)

}

func main() {

c := Car{model: "DeLorean DMC-12"}

defer c.PrintModel()

c.model = "Chevrolet Impala"

}

输出#


DeLorean DMC-12

有指针#


func (c *Car) PrintModel() {

fmt.Println(c.model)

}

输出#


Chevrolet Impala

发生了啥?#

Go

请记住,传递给延迟函数的参数将立即被保存到一边,而无需等到延迟函数运行。

所以,当一个有接收值的方法使用 defer 时,接收方将在注册时被复制 (在本例中是 Car),并且对它的更改将不可见 (Car.model)。因为接收器也是一个输入参数,当它被注册到 defer 时,立即被计算到 “DeLorean DMC-12”。

另一方面,当接收者是一个指针,当它被延迟调用时,一个新的指针被创建,但它指向的地址将与上面的 “c” 指针相同。所以,它的任何变化都能完美地反映出来。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://blog.learngoprogramming.com/gotc...

译文地址:https://learnku.com/go/t/61445

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议