一直没有完全搞懂这个问题。总是被莫名奇妙的 nil pointer dereference 困扰了以后才想起来,不小心把 = 打成 := 了。今天再次遇到这个问题,决心把它弄明白。
我们知道,Go 允许不同作用域内使用同一个变量名,这样重名的两个变量是互不影响的。像这样:
a := "test"
{
a := 1145
fmt.Println(a)
}
fmt.Println(a)
我们也可以在不同的作用域内使用同一个变量。只要不重新定义,默认会逐级往上找,直到找到名称符合的变量。这没什么问题,很显而易见的规则。
不过,Go 的 := 操作符有一个令人迷惑的点:
var a string = "test"
a, b := "test again", 456
fmt.Println(a, b)
这段代码是合法的。我一直以为,Go 会针对已经声明的变量使用赋值操作,对未声明的变量使用声明操作,也就是认为第二行的操作等价于:
a = "test again"
b := 456
事实上,Go 的这种操作称作“重声明”(Redeclaration)。摘录一下官方文档内的描述:
In a := declaration a variable v may appear even if it has already been declared, provided:
- this declaration is in the same scope as the existing declaration of v ( if v is already declared in an outer scope, the declaration will create a new variable § ),
- the corresponding value in the initialization is assignable to v, and
- there is at least one other variable that is created by the declaration.
在使用 ":=" 定义若干变量时,其中某个变量可以是已经被定义过的,只要满足以下条件:
- 这个声明和定义此变量的声明在同一个作用域中( 如果此变量是从外部定义域定义的,那么这个声明将创建一个新的同名变量 );
- 对应的值和此变量原来的类型对应;
- 至少有一个变量是由此声明创建的(就是说,":=" 的左边至少要有一个未定义的变量)。
其实我也没有完全说错嘛,绕了半天这个不能改变类型的“重声明”和单纯赋值不是差不多吗?但是关键的是粗体文字,Go 的 := 操作不会检查其他作用域中的同名变量。若是当前作用域中没有对这个变量的声明,它就会创建一个只在这个作用域内有效的局部变量。因此,以下代码不会进行预期的操作:
package main
import (
"fmt"
"os"
)
var f *os.File
func main() {
openFile()
buf := make([]byte, 16)
n, err := f.Read(buf)
fmt.Println(buf[:n], f, err)
}
func openFile() {
f, err := os.Open("somefile")
if err != nil {
panic(err)
}
fmt.Println(f)
}
我们会发现,在 main 函数中,变量 f 始终是一个空指针。原因便是上面所述的,在 openFile 函数中,变量 f 并不是全局变量,而是新创建的一个局部变量,因此,对这个变量的更改不会在全局变量中体现。
要解决这个问题也很简单,不使用 := 即可。拿上面的例子来说,只需要改成这样:
func openFile() {
var err error
f, err = os.Open("somefile")
// do next things
}
两个函数中的 f 便都是全局变量了。
事实上这是一个比较基础的问题,只是我对此一直迷迷糊糊的。咱就是说,水了一篇文章