zl程序教程

您现在的位置是:首页 >  其他

当前栏目

你知道defer的参数和接收者是如何被取值的吗

2023-02-26 09:48:26 时间

本文是对 《100 Go Mistackes:How to Avoid Them》 一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。

我们在文章使用defer释放资源一文中讲过defer语句是在其所在函数返回后才被执行的。在前面章节中,我们只是用了不带参数的defer调用。然而,如果一个defer函数带有参数,那么这些参数是如何被取值的呢?

本文会深入讨论在defer函数中参数取值以及带指针或值接受者的defer。

1 参数取值

在下面的例子中,我们将实现一个打车的应用程序,其主要功能是为乘客找到一个最合适的司机。我们将实现一个SearchDrivers函数,该函数接收一个drivers列表参数,应用两个过滤器,然后返回一个drivers子集。同时,我们还会使用logStatus和incrementStatusCounter两个函数来用于监测下面这些状态中:

  • 第一个过滤器失败
  • 第二个过滤器失败
  • 所有的过滤器执行成功

为避免重复调用logStatus和incrementStatusCounter,我们会使用defer关键词:

type Status int ①

const (
  StatusSuccess Status = iota
  StatusRadiusFilterError
  StatusActivityFilterError
)

func SearchDrivers(drivers []Driver) ([]Driver, error) {
  var status Status
  defer logStatus(status) ②
  defer incrementStatusCounter(status) ③

  var err error
  drivers, err = applyRadiusFilter(drivers)
  if err != nil {
    status = StatusRadiusFilterError ④
    return nil, err
  }

  drivers, err = applyActivityFilter(drivers)
  if err != nil {
    status = StatusActivityFilterError ⑤
    return nil, err
  }

  status = StatusSuccess ⑥
  return drivers, nil
}

① 定义一个Status类型枚举

② 延迟调用logStatus函数

③ 延迟调用 incrementStatusCounter函数

④ 设置status值为半径过滤错误

⑤ 设置status值为活跃过滤器错误

⑥ 设置status值为成功

首先,我们定义了一个status变量。该变量被同时传递给了logStatus和incrementStatusCounter函数。在整个函数中,依赖于可能的错误,我们更新status变量值。

如果我们尝试执行该函数,logStatus和incrementStatusCounter函数总是会被调用执行,并且status的值都是一样:StatusSuccess。这是为什么呢?

原因就是defer函数的参数是立即被取值的,而非在函数返回时

在这个例子中,我们是调用的 logStatus(status)和incrementStatusCounter(status)作为延迟执行的函数。因此,Go将会使用函数被调用时刻的status值来调度这些函数。因为status是通过var status Status初始化的,那它的当前值就是0,也就是StatusSuccess。

如果我们想继续使用defer,又能取到status最终的值,那我们怎么解决该问题呢?有两种解决方案。

第一种解决方案是给延迟执行的函数传递一个指针。指针保存的是一个变量的内存地址。即使指针值是被立即取值的,但它指向的变量的值是可能会改变的。

func SearchDrivers(drivers []Driver) ([]Driver, error) {
    var status Status
    defer logStatus(&status) ①
    defer incrementStatusCounter(&status) ②

    var err error
    drivers, err = applyRadiusFilter(drivers)
    if err != nil {
      status = StatusRadiusFilterError
      return nil, err
    }

    drivers, err = applyActivityFilter(drivers)
    if err != nil {
      status = StatusActivityFilterError
      return nil, err
    }

    status = StatusSuccess
    return drivers, nil
}

① 延迟执行函数logStatus接收一个Status的指针类型

② 延迟执行函数incrementStatusCounter接收一个Status的指针类型

我们修改logStatus和incrementStatusCounter接收一个 *Status指针,因此我们改变了调用这些函数的方式。其余的实现仍和之前一样。因为status是一个指针,当这两个函数被调度执行时,它将通过引用已更新的status值来完成。

然而,就像我们所说的,这个方案需要改变这两个函数的签名,并不是所有的时候都适用。

第二种方案就是通过闭包的形式来调用延迟语句。闭包是引用其外部变量的函数值。例如:

func f() {
  s := "foo"
  go func() {
    fmt.Println(s) ①
  }()
}

① 在f的函数体外引用了变量s

我们已经介绍过,传给延迟函数的参数是立刻被取值的。然而,通过闭包引用的变量是在执行闭包的时候才取值的(所以,是当函数返回时)

下面是一个演示闭包是如何工作的例子:

func f() {
    i := 0
    j := 0
    defer func(i int) { ①
      fmt.Println(i, j) ②
    }(i) ③
   
    i++
    j++
}

① 一个作为延迟函数的闭包,接收一个整型作为输入

② i是闭包函数的输入,j是闭包外部变量

③ 传递参数i给闭包(i是被调用时的值,即0)

这里,闭包引用了两个变量:i和j。i是作为函数参数传递给闭包的,所以它的值是取当前的值。相反,j是闭包外边的一个变量,所以当闭包被执行时,j的值才会被取到。如果我们运行这个例子,将会输出0和1

因此,我们可以使用闭包来作为SearchDrivers的另一个版本的实现:

func SearchDrivers(drivers []Driver) ([]Driver, error) {
    var status Status
    defer func() { ①
        logStatus(status) ②
        incrementStatusCounter(status) ③
    }() ④
    var err error
    drivers, err = applyRadiusFilter(drivers)
    if err != nil {
        status = StatusRadiusFilterError
        return nil, err
    }
    drivers, err = applyActivityFilter(drivers)
    if err != nil {
        status = StatusActivityFilterError
        return nil, err
    }
    status = StatusSuccess
    return drivers, nil
}
① 将闭包作为延迟函数来调用
② 在闭包中通过引用status变量来调用logStatus函数
③ 在闭包中通过引用status变量来调用incrementStatusCounter
④ 空参数列表

我们将logStatus和incrementStatusCounter的调用封装到了一个没有参数的闭包中。这个闭包引用闭包外部的变量status。因此,我们会使用status的最新的值来调用这两个函数。

现在,使用带指针或值接收者的defer又是怎么样的呢?让我们看下它是如何工作的。

2 带指针和值接受器的defer

当给一个方法指定接收者的时候,这个接收者可以是一个值拷贝,也可以是一个指针。简单来说,就是指针接收器可以修改接收器指向的值。想反,值拷贝接收器是原类型值的一个拷贝。

当我们在一个方法上使用defer时,会执行和参数取值相同的逻辑。使用一个值拷贝作为接收器时,接收器的值是立即被取值的

func main() {
  s := Struct{id: "foo"}
  defer s.print() ①
  s.id = "bar" ②
}

type Struct struct {
  id string
}

func (s Struct) print() {
  fmt.Println(s.id) ③
}

① s是被立即取值的

② 更新s.id(不可见)

③ 输出foo,而非bar

在这个例子中,我们把print方法作为延迟函数来调用。该方法有一个值接收器,因此defer将调度该方法的执行,此时该方法的接收器是一个包含id字段值为foo的结构体。因此,该例子的输出是 foo。

相反,如果接收器是一个指针,通过指针的引用而改变的变量值是可见的:

func main() {
  s := &Struct{id: "foo"}
  defer s.print() ①
  s.id = "bar" ②
}

type Struct struct {
  id string
}

func (s *Struct) print() {
  fmt.Println(s.id) ③
}

① s是一个指针,它理解被取值,但当延迟方法被执行时,它可以引用另外一个变量值

② 更新 s.id(可见)

③ 输出bar

当调用defer时,s指针也是被立即取值的。然而,该指针引用了一个结构体,该结构体的值在函数返回前发生了变化。因此,该实例的输出是bar。

3 小结

总之,在一个方法或函数上调用defer,调用的参数是被立即取值的。对于一个方法来说,接收器也是被立即取值的。如果我们想要延迟取值,可以通过使用指针或闭包的方式来实现。