前言

这是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(变量遮蔽)。

1
2
3
4
5
6
7
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循环里面是不可见的,被遮蔽了。

常见错误

对于下面这段代码,大家思考下,看看会有什么问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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种解决方案。

解决方案1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var client *http.Client
if tracing {
    c, err := createClientWithTracing()
    if err != nil {
        return err
    }
    client = c
} else {
    // Same logic
}

在if/else里定义一个临时变量c,然后把c赋值给变量client

解决方案2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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做判断和处理,代码示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    
    if tracing {
        client, err = createClientWithTracing()
    } else {
        client, err = createDefaultClient()
    }
    if err != nil {
        // Common error handling
    }
    

如何自动发现variable shadowing

靠人肉去排查还是容易遗漏的,Go工具链里有一个shadow命令可以帮助我们排查代码里潜在的variable shadowing问题。

  • 第一步,安装shadow命令

    1
    
    go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
    
  • 第二步,使用shadow检查代码里是否有variable shadowing

    1
    
    go vet -vettool=$(which shadow)
    

比如,我检查后的结果如下:

1
2
3
$ 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源代码文件名。

1
2
3
4
$ 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 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。

发送消息「进群」,和同行一起交流学习,答疑解惑。

References