defer、return及赋值顺序

2019-04-19  本文已影响0人  luckiexie

在使用go语言开发项目时,有这么个需求:在函数返回前检查某个值是否合法,如果不合法则进行修正。那最自然的实现方式如下:

func getNumber() int {
    number := 0
    threshold := 10000
    // do something and assign value to number
    number = 99999
    // do something
    // ... ...

    // check number is valid or not
    if number > threshold {
        number = threshold
    }
    return number
}

但是这种方式有个弊端,当number赋值为99999后,在check前,如果函数返回了,则会漏掉检查,得到不符合预期的值。使用java语言解决这个问题的方法是在finally块中进行检查动作,则不会漏掉检查的逻辑。同样,在golang中也提供了类似的方法---使用defer关键字。defer会在函数返回前执行。利用这一特性,我们可以这样实现:

func getNumber() int {
    threshold := 10000
    number := 0
    
    // check before return
    defer func() {
        // check number is valid or not
        if number > threshold {
            number = threshold
        }
    }()
    
    // do something and assign value to number
    number = 99999
    // do something
    // ... ...
    
    return number
}

func main() {
    fmt.Println(getNumber())
}

main函数里的输出可能有点意外,输出为99999。并没有得到我们预期的结果。那这里的原因是什么呢?其实,return包含了两个步骤:

  1. 给返回值赋值;
  2. 调用RET返回指令并传入返回值,而RET则会检查defer是否存在,若存在就先逆序插播defer语句,最后RET携带返回值退出函数。

这里需要理解下第一步,给返回值赋值时,如果是匿名返回值,则会申明一个变量,并将值赋值给该变量。第二步再将该变量传入RET。因此,对于匿名返回值的情况,最终返回值是在defer执行前就已经确定,即使defer执行修改了number,但是并不会修改返回值的变量。那对于具名返回值,则在return赋值时,是直接对具名变量赋值(因为在函数申明时,返回值变量就已存在),defer修改返回值会生效。如下:

func getNumber() (number int) {
    threshold := 10000
    
    // check before return
    defer func() {
        // check number is valid or not
        if number > threshold {
            number = threshold
        }
    }()

    // do something and assign value to number
    number = 99999
    // do something
    // ... ...

    return number
}

这样实现,便能达到我们想要的效果。因此,在需要在函数返回前修改返回值的情况下,可以考虑使用具名返回值的形式,以防踩坑。

上一篇下一篇

猜你喜欢

热点阅读