这是Go常见错误系列的第11篇:Go语言中意外的变量遮蔽。素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi。
本文涉及的源代码全部开源在:Go常见错误源代码,欢迎大家关注公众号,及时获取本系列最新更新。
变量遮蔽的英文原词是 variable shadowing,我们来看看维基百科上的定义:
In computer programming, variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope. At the level of identifiers (names, rather than variables), this is known as name masking. This outer variable is said to be shadowed by the inner variable, while the inner identifier is said to mask the outer identifier. This can lead to confusion, as it may be unclear which variable subsequent uses of the shadowed variable name refer to, which depends on the name resolution rules of the language.
简单来说,如果某个作用域里声明了一个变量,同时在这个作用域的外层作用域又有一个相同名字的变量,就叫variable shadowing(变量遮蔽)。
func test() {
i := -100
for i := 0; i < 10; i++ {
fmt.Println(i)
}
fmt.Println(i)
}
比如上面这段代码,在for循环里面和外面都有一个变量i
。
for循环里面fmt.Println(i)
用到的变量i
是for循环里面定义的变量i
,for循环外面的i
在for循环里面是不可见的,被遮蔽了。
对于下面这段代码,大家思考下,看看会有什么问题:
var client *http.Client
if tracing {
client, err := createClientWithTracing()
if err != nil {
return err
}
log.Println(client)
} else {
client, err := createDefaultClient()
if err != nil {
return err
}
log.Println(client)
}
// Use client
这段代码逻辑分3步:
- 首先定义了一个变量
client
- 在后面的代码逻辑里,根据不同情况创建不同的client
- 最后使用赋值后的client做业务操作
但是,我们要注意到,在if/else里对client
变量赋值时,使用了:=
。
这个会直接创建一个新的局部变量client
,而不是对我们最开始定义的client
变量进行赋值,这就是variable shadowing现象。
这段代码带来的问题就是我们最开始定义的变量client
的值会是nil
,不符合我们的预期。
那我们应该怎么写代码,才能对我们最开始定义的client
变量赋值呢?有以下2种解决方案。
var client *http.Client
if tracing {
c, err := createClientWithTracing()
if err != nil {
return err
}
client = c
} else {
// Same logic
}
在if/else里定义一个临时变量c
,然后把c
赋值给变量client
。
var client *http.Client
var err error
if tracing {
client, err = createClientWithTracing()
if err != nil {
return err
}
} else {
// Same logic
}
直接先把error
变量提前定义好,在if/else里直接用=
做赋值,而不用:=
。
上面这2种方案其实都可以满足业务需求。我个人比较推荐方案2,主要理由如下:
-
代码会更精简,只需要直接对最终用到的变量做一次赋值即可。方案1里要做2次赋值,先赋值给临时变量
c
,再赋值给变量client
。 -
可以对error统一处理。不需要在if/else里对返回的error做判断,方案2里我们可以直接在if/else外面对error做判断和处理,代码示例如下:
if tracing { client, err = createClientWithTracing() } else { client, err = createDefaultClient() } if err != nil { // Common error handling }
靠人肉去排查还是容易遗漏的,Go工具链里有一个shadow
命令可以帮助我们排查代码里潜在的variable shadowing问题。
-
第一步,安装
shadow
命令go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
-
第二步,使用
shadow
检查代码里是否有variable shadowinggo vet -vettool=$(which shadow)
比如,我检查后的结果如下:
$ go vet -vettool=$(which shadow)
# example.com/shadow
./main.go:9:6: declaration of "i" shadows declaration at line 8
此外,shadow命令也可以单独使用,不需要结合go vet
。shadow后面需要带上package名称或者.go源代码文件名。
$ shadow example.com/shadow
11-variable-shadowing/main.go:9:6: declaration of "i" shadows declaration at line 8
$ shadow main.go
11-variable-shadowing/main.go:9:6: declaration of "i" shadows declaration at line 8
- 遇到variable shadowing的情况,我们需要小心,避免出现上述例子里的情况。
- 可以结合
shadow
工具做variable shadowing的自动检测。
文章和示例代码开源在GitHub: Go语言初级、中级和高级教程。
公众号:coding进阶。关注公众号可以获取最新Go面试题和技术栈。
个人网站:Jincheng's Blog。
知乎:无忌。
我为大家整理了一份后端开发学习资料礼包,包含编程语言入门到进阶知识(Go、C++、Python)、后端开发技术栈、面试题等。
关注公众号「coding进阶」,发送消息 backend 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。
发送消息「进群」,和同行一起交流学习,答疑解惑。