在使用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包含了两个步骤:
- 给返回值赋值;
- 调用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
}
这样实现,便能达到我们想要的效果。因此,在需要在函数返回前修改返回值的情况下,可以考虑使用具名返回值的形式,以防踩坑。