Go 变量遮蔽

0 阅读4分钟
part 1.

短变量声明不总是会声明一个新变量。

e.g.

func main() {
 var a = 1
 ab := 23
 fmt.Println(a, b)
}

按照我们对声明语句的传统理解,上面第二行的多变量短声明语句重新声明了变量 a 和 b,我们用下面代码来确认一下:

func main() {
 var a = 1
 fmt.Println(&a) // 0xc00000a0b0
 ab := 23
 fmt.Println(&a) // 0xc00000a0b0
 fmt.Println(a, b)
}

从输出结果来看,我们对声明语句的传统理解似乎 “失效” 了。多变量短声明语句 (multi-variable short declaration) 并未重新声明一个新变量 a,它只是给之前已经声明了的变量 a 做了重新赋值。

这是一个典型的由于认知偏差而形成的 “陷阱”。Go 规范针对此 “陷阱” 有明确的说明:在同一个代码块 (block) 中,使用多变量短声明语句重新声明已经声明过的变量时,短变量声明语句不会为该变量声明一个新变量,而只是会对其做重新赋值。

如果不在同一个代码块 (block) 中呢?

part 2.

在不同代码块 (block) 层次上使用多变量短声明形式会带来 “难以发现” 的变量遮蔽问题。

e.g.

var cwd string

func init() {
 cwd, err := os.Getwd()
 if err != nil {
  log.Fatalf("os.Getwd failed: %v", err)
 }
 log.Printf("Working directory = %s", cwd)
}

虽然 cwd 在外部已经声明过,但是 := 语句还是将 cwd 和 err 重新声明为新的局部变量。因为内部声明的 cwd 将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的 cwd 变量。

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明 err 变量,来避免使用 := 的简短声明方式:

var cwd string

func init() {
 var err error
 cwd, err = os.Getwd()
 if err != nil {
  log.Fatalf("os.Getwd failed: %v", err)
 }
}

根本原因是因为,当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。

如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。

在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问 (也就是说,如果一个标识符被遮挡了,则它的作用域将不包括遮挡它的标识符的作用域)。

e.g.

func foo() (interror) {
 return 11nil
}

func bar() (interror) {
 return 21, errors.New("error in bar")
}

func main() {
 var err error
 defer func() {
  if err != nil {
   println("error in defer:", err.Error())
  }
 }()

 a, err := foo()
 if err != nil {
  return
 }
 println("a =", a)

 if a == 11 {
  b, err := bar()
  if err != nil {
   return
  }
  println("b =", b)
 }
 println("no error occurs")
}

对于上面这个示例,我们期待输出下面结果:

a = 11
error in defererror in bar

实际:

a = 11

b, err := bar() 这行代码后会误以为 err 不会被重新声明为一个新变量,仅会做赋值操作,就像前面 part 1 短声明变量的“陷阱” 中描述的那样。

但实际上,由于不在同一个代码块 (block) 中,编译器没有在同一代码块里找到与 b, err := bar() 这行代码中 err 同名的变量,因此会声明一个新 err 变量,该 err 变量也就 “顺理成章” 地遮蔽了 main 函数代码块中的 err 变量。

同上,修正这个问题的方法有很多,但最直接的方法就是去掉 if 代码块中的多变量短声明形式并提前单独声明变量 b。

e.g.

// 同理 shadow(), shadow2() 都是错误示例
func shadow() (err error) {
 x, err := check1() // x是新创建变量,err是被赋值
 if err != nil {
  return // 正确返回err
 }
 if y, err := check2(x); err != nil { // y和if语句中err被创建
  return // if语句中的err覆盖外面的err,所以错误的返回nil!
 } else {
  fmt.Println(y)
 }
 return
}

func shadow2() (err error) {
 if err := check3(); err != nil {
  return
 }
 return
}

func check1() (interror) {
 return 1nil
}

func check2(x int) (interror) {
 return 0, errors.New("error in bar")
}

func check3() error {
 return errors.New("error in bar")
}

func main() {
 fmt.Println(shadow())
 fmt.Println(shadow2())
}