Go可见性规则决定编译期标识符访问权限,happens-before规则保障运行时多goroutine内存操作顺序与可见性;二者不可混用,导出变量仍需同步机制避免竞态。

Go 的可见性规则和 happens-before 规则不是一回事,但混用时极易出错——前者决定编译期能否访问标识符,后者决定运行时多 goroutine 间内存操作的顺序与可见性。没理清这点,sync.Mutex 白加,atomic.LoadUint64 也救不了你。
Go 标识符首字母大小写直接决定包级可见性
Go 没有 public/private 关键字,可见性全靠命名:首字母大写 = 导出(exported),小写 = 包内私有(unexported)。这个规则在编译期硬编码,不依赖任何运行时机制。
-
func DoWork()可被其他包调用;func doWork()只能在定义它的包里用 - 结构体字段同理:
type User struct { Name string }中Name可被外部读写,name string不行 - 即使通过反射绕过编译检查,也无法突破运行时对未导出字段的访问限制(
reflect.Value.Interface()对未导出字段 panic)
happens-before 不是 Go 特有概念,但 Go 内存模型只保证特定同步原语下的顺序
Go 内存模型不承诺“自然顺序”跨 goroutine 可见。比如一个 goroutine 写 x = 1 后启动另一个 goroutine 读 x,读到 0 完全合法——除非存在 happens-before 关系。
- 唯一明确建立 happens-before 的方式是使用 Go 官方认可的同步事件:channel 收发、
sync.Mutex.Lock/Unlock、sync.WaitGroup.Done/Done、atomic操作等 -
time.Sleep(1 * time.Second)不能替代同步:它不建立 happens-before,只是碰巧让写操作“大概率”完成 - 同一个 mutex 上的连续
Lock–Unlock对之间有 happens-before,但不同 mutex 之间无传递性
常见误用:用可见性规则代替同步,或以为同步能改变标识符可见性
这两类错误在真实代码里高频出现,且往往只在压测或特定调度下暴露。
- 把
var counter int设为导出变量(Counter),就认为其他包“能安全读写”——错。导出只解决访问权限,不解决竞态;必须配sync.Mutex或atomic - 在 goroutine 里修改未导出字段(如
u.name = "alice"),然后期望主 goroutine 通过反射或 unsafe 读到新值——错。没有同步原语,读操作可能永远看不到写,或看到部分写入(撕裂) - 用
sync.Once初始化全局变量,却忘了初始化函数内部的赋值也要满足 happens-before(通常没问题,因为Once.Do本身提供同步)
最易被忽略的一点:Go 编译器和 CPU 都可能重排序,而 happens-before 是唯一能压制这种重排序的契约。别信“我加了导出,又加了 mutex,应该稳了”——mutex 的 Lock 和 Unlock 必须包裹所有共享数据的访问,漏一次,整个同步就失效。










