Go 语言变量作用域

包级、函数级、块级、以及遮蔽带来的隐蔽 bug。

Beginner
Key Points变量在哪个 `{}` 里声明,就活到那个 `{}` 结束;包级变量活到程序结束。

三个层级

Go
package main

import "fmt"

var global = "包级"          // 整个包可见

func main() {
	local := "函数级"
	{
		block := "块级"
		fmt.Println(global, local, block)
	}
	// fmt.Println(block)     // 编译错误:undefined
	fmt.Println(global, local)
}
Output
包级 函数级 块级
包级 函数级
LevelLifetimeVisible to
包级程序运行期同包所有文件
函数级函数调用期间函数体
块级块执行期间该 {} 内

遮蔽

Go
x := 1
if true {
	x := 2                 // 新变量,遮蔽外层
	fmt.Println("内", x)
}
fmt.Println("外", x)
Output
内 2
外 1

err 遮蔽的经典坑

Go
func bad() error {
	var err error

	if true {
		_, err := os.Open("nope.txt")   // := 新建了一个 err!
		_ = err
	}

	return err                          // 永远是 nil
}

func good() error {
	var err error

	if true {
		_, err = os.Open("nope.txt")    // = 赋值给外层 err
	}

	return err                          // 正确返回错误
}
One of Go's nastiest bugs: := inside if/for/switch creates a new variable, the outer err stays nil, and the function "succeeds". Catch it with the shadow vet tool or your IDE.

包级变量的初始化顺序

Go
var a = b + 1        // 依赖 b
var b = 2

func init() {
	fmt.Println("init 执行")
}

func main() {
	fmt.Println(a, b)
}
Output
init 执行
3 2

Go topologically sorts package-level initialisation, so a can use a b declared later. init() runs after all variables and before main().

什么时候用包级变量

  • 用 —— 全局配置、日志器、单例连接池(且用 sync.Once 保护)
  • 不用 —— 跨函数传状态。传参更清楚,也更好测试
  • 不用 —— 可变全局状态。并发下必然出问题
Go
var (
	once   sync.Once
	client *http.Client
)

func getClient() *http.Client {
	once.Do(func() {
		client = &http.Client{Timeout: 10 * time.Second}
	})
	return client
}

Friends & Partners