一直没有完全搞懂这个问题。总是被莫名奇妙的 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 便都是全局变量了。

事实上这是一个比较基础的问题,只是我对此一直迷迷糊糊的。咱就是说,水了一篇文章