Go语言中的变量遮蔽(Variable Shadowing)

首先,看一段Go语言的代码:


package main

import (
	"fmt"
)

var a = 11

func foo(n int) {
	a := 1
	a += n
}

func main() {
	fmt.Println("a =", a) // 11
	foo(5)
	fmt.Println("after calling foo, a =", a) 
}

可以猜想,最后的是“after calling foo, a = 6”

可是!事实并非如此: alt

最终的第二行输出是:“after calling foo, a = 11”

为什么会发生这种事情呢?

主要的原因是:虽然 foo 函数中也使用了变量 a,但是 foo 函数中的变量 a 遮蔽了外面的包级变量 a,这使得包级变量 a 没有参与到 foo 函数的逻辑中,所以就没有发生变化了。

如果将foo函数中a := 1改成 a = 1, 那么结果就是“after calling foo, a = 6”

所以这种变量遮蔽的情况是很难发现的,而且需要花费非常多的时间进行debug

再举例:

package main

import (
	"errors"
	"fmt"
)

var a int = 2020

func checkYear() error {
	err := errors.New("wrong year")

	switch a, err := getYear(); a {
	case 2020:
		fmt.Println("it is", a, err)
	case 2021:
		fmt.Println("it is", a)
		err = nil
	}
	fmt.Println("after check, it is", a)
	return err
}

type year int

func getYear() (year, error) {
	var b int16 = 2021
	return year(b), nil
}

func main() {
	err := checkYear()
	if err != nil {
		fmt.Println("call checkYear error:", err)
		return
	}
	fmt.Println("call checkYear ok")
}


这段代码也没有按照作者本身的想法执行,本意是想正确核实2021,并且不会发生报错,但是又因为变量遮蔽的情况,使得输出如下: alt

问题分析: 1、位于第 7 行的 switch 语句在它自身的隐式代码块中,通过短变量声明形式重新声明了一个变量 a,这个变量 a 就遮蔽了外层包代码块中的包级变量 a,这就是打印“after check, it is 2020”的原因。包级变量 a 没有如预期那样被 getYear 的返回值赋值为正确的年份 2021,2021 被赋值给了遮蔽它的 switch 语句隐式代码块中的那个新声明的 a。

2、同时,在第 7 行的 switch 语句中,除了声明一个新的变量 a 之外,它还声明了一个名为 err 的变量,这个变量就遮蔽了第 4 行 checkYear 函数在显式代码块中声明的 err 变量,这导致第 12 行的 nil 赋值动作作用到了 switch 隐式代码块中的 err 变量上,而不是外层 checkYear 声明的本地变量 err 变量上,后者并非 nil,这样 checkYear 虽然从 getYear 得到了正确的年份值,但却返回了一个错误给 main 函数,这直接导致了 main 函数打印了错误:“call checkYear error: wrong year”。

正确版本如下:

package main

import (
	"errors"
	"fmt"
)

var a int = 2020

func checkYear() error {
	err := errors.New("wrong year")

	a, err := getYear()
	switch a {
	case 2020:
		fmt.Println("it is", a, err)
	case 2021:
		fmt.Println("it is", a)
		err = nil
	}
	fmt.Println("after check, it is", a)
	return err
}

type year int

func getYear() (year, error) {
	var b int16 = 2021
	return year(b), nil
}

func main() {
	err := checkYear()
	if err != nil {
		fmt.Println("call checkYear error:", err)
		return
	}
	fmt.Println("call checkYear ok")
}

日常中,我们可以安装go vet 工具对代码进行静态检查,变量遮蔽检查的插件安装如下:

$go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go: downloading golang.org/x/tools v0.1.5
go: downloading golang.org/x/mod v0.4.2

安装成功之后,我们可以通过 go vet 扫描代码并检查这里面有没有变量遮蔽的问题了

$go vet -vettool=$(which shadow) -strict complex.go 
./complex.go:13:12: declaration of "err" shadows declaration at line 11

但是该工具并不是万无一失的,也存在遗漏的情况,例如本样例中的变量 a就并没有被检查出来。所以,为了减少变量遮蔽情况的发生,我们还是要深入理解代码块与作用域的概念,尽可能在日常编码时就主动规避掉所有遮蔽问题。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务