zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Go 100 mistakes之意外的变量隐藏(variable shadowing)

2023-02-26 09:48:24 时间

变量的作用域是指它的可见性。换句话说,程序中的变量名在哪部分是有效的。在Go中,在一个块中声明的变量名称可以在其内部块中重新声明,这被称作变量隐藏(variable shadowing)。然而这种规则由很容易出现错误。

在下面的例子中,将会看到一个关于变量隐藏而产生的bug。我们将使用两种不同的方式创建一个HTTP客户端,具体取决于tracing布尔值:

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

① 声明一个client变量

② 创建一个带tracing的HTTP客户端,client变量在该块内被隐藏了

③ 创建一个默认的HTTP客户端,client变量在该模块依然被隐藏掉了。

首先,我们声明了一个client变量。然后,在两个内部块中,我们使用 := 操作符,也叫做短变量声明运算符。该操作符使用和开始的时候相同的名称创建了一个新的client变量;它不会为第①行中的client变量赋值。因此,在该示例中,HTTP客户端将始终是nil值。

注意:该代码之所以可以编译成功,是因为在logging调用中使用了内部变量client。否则,我们就会有编译错误:client declared and not used。

我们如何确保给client赋值了呢?有两种不同的方法。

第一种方法是在内部块中使用临时变量,像下面这样:

var client *http.Client
if tracing {
    c, err := createClientWithTracing() ①
    if err != nil {
    return err
    }
    client = c ②
} else {
    c, err := createDefaultClient()
    if err != nil {
        return err
    }
    client = c
}
// Use client

① 创建了一个临时变量c

② 将临时变量赋给变量client。

变量c的生命周期只在if/else块中。然后,我们将这些变量赋值给client。

第二种方式是在内部块中使用赋值操作符(=)来将函数的返回值直接赋值给client变量。然而,它需要创建一个error变量,因为赋值运算符仅在已声明变量时才起作用。

var client *http.Client
var err error ①
if tracing {
  client, err = createClientWithTracing() ②
  if err != nil {
    return err
  }
} else {
  client, err = createDefaultClient()
  if err != nil {
    return err
  }
}

① 声明变量err

② 使用赋值操作符将返回来的*http.Client直接赋值给client变量

在这个例子中,我们也将内部调用的结果赋值给了client。

哪种方法最好呢?第一种方法在大多数情况下都是更方便的,但是没有强迫说要是用哪种方法。

当在内部块中将一个变量名重新声明时就会发生变量隐藏。我们已经看到这种做法很容易出错。应根据项目和上下文制定避免隐藏变量的规则。例如,有时候,重用现有的变量名可能会很方便,像err错误。然而,一般来说,我们应该保持谨慎,因为我们已经看到我们可能会面临这样一种错误:代码可以编译,但可能不会对我们期望的变量进行赋值。

那我们有没有办法自动化检查呢?使用vet和shadow工具

$ go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow 
go: finding golang.org/x/tools latest
go: downloading golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2
go: extracting golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2
$ go vet -vettool=$(which shadow) 
./main.go:8:3: declaration of "i" shadows declaration at line 6

① shadow安装

② 使用vettol参数将shadow链接到vet

③ Go vet可以检测隐藏的变量了

package main

import "fmt"

func main() {
    i := 0
    if true {
        i := 1 ①
    }
    fmt.Println(i)
}

① 隐藏的变量

注意,在执行go vet -vettool=$(which shadow) 时如果which shadow找不到路径,可以将其换成shadow的绝对路径。使用go install安装shadow命令默认安装在GOPATH下的bin目录中:GOPATH/bin/shadow。

小结

在Go中,允许在代码块中声明和外部代码块中相同名称的变量,但此时,子代码块中的变量会覆盖外部的变量,这被称为变量隐藏(variable shadowing)。避免变量隐藏的方法可以通过在内部块中使用临时变量,或者在内部块中直接给外部变量赋值以避免名称冲突。同时我们也可以使用vet+shadow工具来检测代码中是否有隐藏的变量。