【Go常见错误】8. 关于defer的参数和接受者的取值

前言

defer语句是在其所在函数返回后才被执行的。

然而,如果一个defer函数带有参数,那么这些参数是如何被取值的呢?

defer带参数取值-案例

下面的代码中为避免重复调用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带参数取值-解决方法

如果我们想继续使用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值来完成。

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

第二种方案就是通过闭包的形式来调用延迟语句

闭包是引用其外部变量的函数值。

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

因此,我们可以使用闭包来作为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带指针和值接受器-案例

当给一个方法指定接收者的时候,这个接收者可以是一个值拷贝,也可以是一个指针。

简单来说,就是指针接收器可以修改接收器指向的值。想反,值拷贝接收器是原类型值的一个拷贝。

当我们在一个方法上使用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。

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(可见)

③ 输出bar

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

总结

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值