【问题标题】:What happens when defer is called twice on same variable?当对同一个变量调用两次 defer 时会发生什么?
【发布时间】:2015-03-06 06:46:02
【问题描述】:

当该方法的结构被更改时,defer 被调用两次时发生了什么?

例如:

rows := Query(`SELECT FROM whatever`)
defer rows.Close()
for rows.Next() { 
  // do something
}
rows = Query(`SELECT FROM another`) 
defer rows.Close()
for rows.Next() {
  // do something else
}

最后一个rows.Close() 调用哪个rows

【问题讨论】:

    标签: go


    【解决方案1】:

    这取决于方法接收器变量的类型。

    简短回答:如果您使用 database/sql 包,您的延迟 Rows.Close() 方法将正确关闭您的两个 Rows 实例,因为 Rows.Close() 具有 指针 接收器 因为DB.Query() 返回一个指针(所以rows 是一个指针)。请参阅下面的推理和解释。

    为避免混淆,我建议使用不同的变量,这将清楚您想要什么以及将要关闭什么

    rows := Query(`SELECT FROM whatever`)
    defer rows.Close()
    // ...
    rows2 := Query(`SELECT FROM whatever`)
    defer rows2.Close()
    

    我想指出一个重要的事实,即来自延迟函数及其被立即评估的参数,这在 Effective Go 博客文章和 Language Spec: Deferred statements 中也有说明:

    每次执行“defer”语句时,调用的函数值和参数都会像往常一样进行评估并重新保存,但不会调用实际函数。相反,延迟函数会在周围函数返回之前立即调用,与它们被延迟的顺序相反。

    如果变量不是指针:调用延迟方法时会观察到不同的结果,具体取决于方法是否有指针接收器。
    如果变量是指针,您将始终看到“想要的”结果。

    看这个例子:

    type X struct {
        S string
    }
    
    func (x X) Close() {
        fmt.Println("Value-Closing", x.S)
    }
    
    func (x *X) CloseP() {
        fmt.Println("Pointer-Closing", x.S)
    }
    
    func main() {
        x := X{"Value-X First"}
        defer x.Close()
        x = X{"Value-X Second"}
        defer x.Close()
    
        x2 := X{"Value-X2 First"}
        defer x2.CloseP()
        x2 = X{"Value-X2 Second"}
        defer x2.CloseP()
    
        xp := &X{"Pointer-X First"}
        defer xp.Close()
        xp = &X{"Pointer-X Second"}
        defer xp.Close()
    
        xp2 := &X{"Pointer-X2 First"}
        defer xp2.CloseP()
        xp2 = &X{"Pointer-X2 Second"}
        defer xp2.CloseP()
    }
    

    输出:

    Pointer-Closing Pointer-X2 Second
    Pointer-Closing Pointer-X2 First
    Value-Closing Pointer-X Second
    Value-Closing Pointer-X First
    Pointer-Closing Value-X2 Second
    Pointer-Closing Value-X2 Second
    Value-Closing Value-X Second
    Value-Closing Value-X First
    

    Go Playground 上试试。

    使用指针变量,结果总是好的(如预期的那样)。

    使用非指针变量和指针接收器我们看到相同的打印结果(最新),但如果我们有值接收器,它会打印 2 个不同的结果。

    非指针变量说明:

    如前所述,当defer 执行时,会评估包含接收器的延迟函数。如果是指针接收器,它将是局部变量的地址。因此,当您为其分配一个新值并调用另一个defer 时,指针接收器将再次成为局部变量的相同地址(只是指向的值不同)。所以稍后在执行函数时,两者都会使用相同的地址两次,但 pointed 的值将是相同的,即稍后分配的那个。

    如果是值接收器,接收器是一个副本,它是在defer 执行时生成的,所以如果你为变量分配一个新值并调用另一个defer,另一个副本将与之前的不同。

    【讨论】:

    • 我错过了规范的那部分。好的。 +1
    • defer复制指针是如何实现的?只需创建一个与xpxp2 具有相同值的新指针,与Close()CloseP() 无关?
    • @Hunger 是的,指针只是一个内存地址。当您复制像p2 := p 这样的指针时,您只需复制指向同一对象的内存地址。要复制 pointed 对象,您必须 dereference 指针,例如obj2 := *p.
    【解决方案2】:

    Effective Go 提及:

    延迟函数的参数(如果函数是方法,则包括接收者)在延迟执行时评估,而不是在调用执行时评估

    除了避免担心在函数执行时变量会改变值,这意味着单个延迟调用站点可以延迟多个函数执行

    在您的情况下,延迟将引用第二行实例。
    两个延迟函数按 LIFO 顺序执行(在“Defer, Panic, and Recover”中也提到过)。

    正如iczahis answerin the comments 中提到的那样:

    2 个延迟的 Close() 方法将引用 2 个不同的 Rows 值,并且两者都将正确关闭,因为 rows指针,而不是值类型.

    【讨论】:

      【解决方案3】:

      啊,我明白了,rows 总是指最后一个,http://play.golang.org/p/_xzxHnbFSz

      package main
      
      import "fmt"
      
      type X struct {
         A string
      }
      
      func (x *X) Close() {
        fmt.Println(x.A)
      }
      
      func main() {
        rows := X{`1`}
        defer rows.Close()
        rows = X{`2`}
        defer rows.Close()
      }
      

      输出:

      2
      2
      

      所以也许保存对象的最好方法是将它传递给一个函数:http://play.golang.org/p/TIMCliUn60

      package main
      
      import "fmt"
      
      type X struct {
          A string
      }
      
      func (x *X) Close() {
          fmt.Println(x.A)
      }
      
      func main() {
          rows := X{`1`}
          defer func(r X) { r.Close() }(rows)
          rows = X{`2`}
          defer func(r X) { r.Close() }(rows)
      }
      

      输出:

      2
      1
      

      【讨论】:

      • 最好的方法是使用不同的变量:rows := X{"1"}; rows2 := X{"2"} - 更简单更干净。 Intent 也更清楚你要关闭rowsrows2
      【解决方案4】:

      大多数时候,你应该可以只添加一个块,这样你就不必担心想一个新的变量名,也不必担心没有任何项目关闭:

      rows := Query(`SELECT FROM whatever`)
      defer rows.Close()
      for rows.Next() { 
         // do something
      }
      {
         rows := Query(`SELECT FROM another`) 
         defer rows.Close()
         for rows.Next() {
            // do something else
         }
      }
      

      https://golang.org/ref/spec#Blocks

      【讨论】:

        猜你喜欢
        • 2022-12-11
        • 2021-09-14
        • 2018-01-10
        • 2014-03-18
        • 2013-10-05
        • 1970-01-01
        • 2020-11-15
        相关资源
        最近更新 更多