golang defer的使用

golang defer的使用

基本使用

在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

为了更好的学习defer的行为,我们首先来看下面一段代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

这段代码可以运行,但存在’安全隐患’。如果调用dst, err := os.Create(dstName)失败,则函数会执行return退出运行。但之前创建的src(文件句柄)没有被释放。

上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。而使用defer则可以避免这种情况的发生,下面是使用defer的代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。

规则一

当defer被声明时,其参数就会被实时解析。

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

真正的输出结果是0。

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i)。但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。换言之,上面的代码等同于下面的代码:

func a() {
    i := 0
    defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
    i++
    return
}

为了更为明确的说明这个问题,我们继续定义一个defer:

func a() {
    i := 0
    defer fmt.Println(i) //输出0,因为i此时就是0
    i++
    defer fmt.Println(i) //输出1,因为i此时就是1
    return
}

通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出1,在输出0呢? 看下面的规则二。

规则二

defer执行顺序为先进后出。

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。

我们用下面的代码加深记忆和理解:

func b() {
    for i := 0; i < 4; i++ {
    defer fmt.Print(i)
    }
}

在循环中,依次定义了四个defer代码块。结合规则一,我们可以明确得知每个defer代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了3210.

规则三

defer可以读取有名返回值。

先看下面的代码:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

输出结果是12. 在开头的时候,我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢….)。

当执行return 1之后,i的值就是1。此时此刻,defer代码块开始执行,对i进行自增操作。因此输出2。

Defer的一些坑

defer nil 函数

如果一个延迟函数被赋值为 nil ,运行时的 panic 异常会发生在外围函数执行结束后而不是 defer 的函数被调用的时候。

例子

func() {
  var run func() = nil
  defer run()

  fmt.Println("runs")
}

名为 func 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在外围函数运行完成后它才会被调用。

上面只是一个简单的案例,但同样的案例也可能发生在真实世界中,所以如果你遇上的话,可以想想是不是掉进了这个坑里。

延迟调用含有闭包的函数

有时出于某种缘由,你想要让那些闭包延迟执行。例如,连接数据库,然后在查询语句执行过后中断与数据库的连接。

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
*/

最终 disconnect 并没有输出,最后只有 connect ,这是一个 bug,最终的情况是 connect() 执行结束后,其执行域得以被保存起来,但内部的闭包并不会被执行。

解决方法

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

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

稍作修改后, db.connect() 返回了一个函数,然后我们再对这个函数使用 defer 就能够在 func() 执行结束后断开与数据库的连接。

特殊的糟糕样例

即便这种处理方式很糟,但我还是想告诉你如何不用变量来解决这个问题,因此,我希望你能以此来了解 defer 亦或是 go 语言的运行机制。

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

  ..
}

这段代码从技术层面上说与上面的解决方案没有本质区别。其中,第一个圆括号是连接数据库(在 defer db.connect() 中立即执行的部分),然后第二个圆括号是为了在 func() 结束时延迟执行断开连接的函数(也就是返回的闭包)。

归因于 db.connect() 创建了一个闭包类型的值,然后再使用 defer 声明闭包函数, db.connect() 的值需要被实现计算出来以便让 defer 知道需要延迟哪个函数,这与 defer 不直接相关但也可能帮助你解决一些问题。

在执行块中使用 defer

你可能想要在执行块执行结束后执行在块内延迟调用的函数,但事实并非如此,它们只会在块所属的函数执行结束后才被执行,这种情况适用于所有的代码块除了上文的函数块例如,for,switch 等。

因为:延迟是相对于一个函数而非一个代码块

例子

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 调用的区域)后执行,就像代码中的演示的那样,你可以使用花括号创造单独的执行块。

解决方案

如果你希望在另一个块中使用 defer ,可以使用匿名函数。

func main() {
  func() {
    defer func() {
      fmt.Println("func: defer runs")
    }()

    fmt.Println("func: ends")
  }()

  fmt.Println("main: ends")
}

延迟方法的坑

同样,你也可以使用 defer 来延迟 方法 调用,但也可能出一些岔子。

没有使用指针作为接收者

ype 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” 指针的地址相同。因此,任何修改都会完美地作用在同一个对象中。

猜你喜欢

转载自blog.csdn.net/u013007900/article/details/80959070