
go语言不会隐式保护共享变量,上述代码存在严重数据竞争问题;看似输出100只是侥幸,实际行为未定义,必须显式使用互斥锁、原子操作或通道等同步机制保障并发安全。
go语言不会隐式保护共享变量,上述代码存在严重数据竞争问题;看似输出100只是侥幸,实际行为未定义,必须显式使用互斥锁、原子操作或通道等同步机制保障并发安全。
在Go中,并发(goroutine)不等于并行(parallelism),更不等于线程安全。你提供的示例代码:
var m int
func add(i int) {
m++
}
func main() {
m = 0
for i := 0; i < 100; i++ {
go add(i)
}
time.Sleep(time.Millisecond * 1000)
fmt.Println(m)
}表面上每次输出 100,容易让人误以为 Go “自动处理了并发写入”。但事实恰恰相反:Go 不提供任何隐式同步机制——m++ 是非原子操作(读取→修改→写入三步),多个 goroutine 同时执行它会引发典型的数据竞争(data race)。
你可以用 Go 内置的竞态检测器验证这一点:
go run -race main.go
运行后将立即报告类似如下的警告(如答案中所示),明确指出两个 goroutine 在无同步下同时读写同一内存地址 0x000000595200,证实了未定义行为的存在。
立即学习“go语言免费学习笔记(深入)”;
⚠️ 关键提醒:输出 100 并非“正确”,而是偶然结果——它依赖于调度时机、CPU缓存一致性、编译器优化甚至运行环境噪声。在高负载、不同架构(如ARM)、启用GC调优或升级Go版本后,结果很可能变为 97、99 甚至崩溃。
✅ 正确做法:显式同步
方案1:使用 sync.Mutex
package main
import (
"fmt"
"sync"
"time"
)
var (
m int
mutex sync.Mutex
)
func add(i int) {
mutex.Lock()
m++
mutex.Unlock()
}
func main() {
m = 0
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
add(i)
}(i)
}
wg.Wait()
fmt.Println(m) // 稳定输出 100
}方案2:使用 sync/atomic(推荐用于简单整型操作)
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var m int64 // 注意:atomic 操作要求对齐,int64 更安全
func add(i int) {
atomic.AddInt64(&m, 1)
}
func main() {
m = 0
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
add(i)
}(i)
}
wg.Wait()
fmt.Println(atomic.LoadInt64(&m)) // 输出 100,无竞争,零分配
}方案3:使用 channel(符合 Go “不要通过共享内存来通信”哲学)
func main() {
ch := make(chan int, 100)
for i := 0; i < 100; i++ {
go func() { ch <- 1 }()
}
sum := 0
for i := 0; i < 100; i++ {
sum += <-ch
}
fmt.Println(sum) // 100
}总结
- ❌ Go 绝不隐式同步,所有共享变量访问都需开发者显式保障安全性;
- ✅ 始终启用
-race标志进行开发与测试(go test -race/go run -race); - ? 优先选择
atomic(轻量、高性能)或Mutex(通用、可组合),避免裸写共享变量; - ? 竞态不是“小概率bug”,而是确定性的未定义行为——它迟早会在生产环境爆发。
记住 Go 的并发格言:
“Do not communicate by sharing memory; instead, share memory by communicating.”
—— 用 channel 协调,用锁/原子操作保护,而非依赖运气。


















