Go变量作用域由声明位置严格限定,函数内声明的变量仅在该函数体可见;if/for块内声明的变量不可被外层访问;同名变量遮蔽、包级初始化顺序、值/指针接收者差异是常见陷阱。
Go 中变量作用域由声明位置严格限定,不是靠大括号“看起来在哪”来判断,而是看它是否在当前代码块的词法嵌套层级中被声明。函数参数、返回值、func 内部用 := 或 var 声明的变量,只在该函数体内可见。
常见错误是误以为 if 或 for 里的变量能被外层访问:
func example() {
if
true {
x := 42
}
fmt.Println(x) // 编译错误:undefined: x
}
x 在 if 块内声明,作用域仅限该块,哪怕 if 条件恒为真也不行var 的 hoisting 行为在内层作用域用相同名字重新声明变量,会遮蔽外层同名变量——这不是错误,但极易导致预期外的行为,尤其在嵌套 if 或 for 中。
func shadow() {
x := "outer"
if true {
x := "inner" // 新变量,遮蔽外层 x
fmt.Println(x) // "inner"
}
fmt.Println(x) // "outer" —— 外层 x 未被修改
}
:= 或 var x string),赋值(x = "new")不会创建新变量go vet 可检测部分遮蔽,但不覆盖所有场景;建议开启 staticcheck 或 revive 的 shadow 检查for _, v := range items { v := v } 是常见冗余遮蔽,通常应删掉内层声明包级变量(在函数外用 var 声明)在包初始化阶段按源码顺序初始化,而 init() 函数在所有变量初始化后执行。若在 init() 中访问尚未初始化的包级变量,会得到零值,且无编译错误。
var a = func() int { return b + 1 }() // b 还没初始化,a = 1(b 是 int 零值)
var b = 100
func init() {
fmt.Println(a) // 输出 1,不是 101
}
go build 解析的文件顺序,不可控;避免在包级初始化中依赖其他包级变量init() 函数中显式赋值,或用惰性初始化(sync.Once + 函数)作用域规则延伸到方法接收者:值接收者(func (s S) Method())操作的是结构体副本,无法修改原始字段;指针接收者(func (s *S) Method())才能写入。这和变量作用域无关,但常被初学者混淆为“作用域限制了修改权限”。
type Counter struct{ n int }
func (c Counter) Inc() { c.n++ } // 无效:修改的是副本
func (c *Counter) IncPtr() { c.n++ } // 有效:通过指针修改原值
func main() {
var c Counter
c.Inc()
fmt.Println(c.n) // 0
c.IncPtr()
fmt.Println(c.n) // 1
}