本文是对 《100 Go Mistackes:How to Avoid Them》 一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。
我们在文章使用defer释放资源一文中讲过defer语句是在其所在函数返回后才被执行的。在前面章节中,我们只是用了不带参数的defer调用。然而,如果一个defer函数带有参数,那么这些参数是如何被取值的呢?
本文会深入讨论在defer函数中参数取值以及带指针或值接受者的defer。
在下面的例子中,我们将实现一个打车的应用程序,其主要功能是为乘客找到一个最合适的司机。我们将实现一个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又是怎么样的呢?让我们看下它是如何工作的。
当给一个方法指定接收者的时候,这个接收者可以是一个值拷贝,也可以是一个指针。简单来说,就是指针接收器可以修改接收器指向的值。想反,值拷贝接收器是原类型值的一个拷贝。
当我们在一个方法上使用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,调用的参数是被立即取值的。对于一个方法来说,接收器也是被立即取值的。如果我们想要延迟取值,可以通过使用指针或闭包的方式来实现。