直接在http handler中修改全局变量会引发竞态,因goroutine并发执行“读—改—写”导致计数偏少、map panic或结果不一致;应仅对临界区加锁,锁变量须为包级或结构体字段,避免误用mutex或滥用rwmutex,复杂状态宜用channel串行化,简单计数优先用atomic。

为什么直接在HTTP handler里改全局变量会出错
因为每个HTTP请求都在独立goroutine里执行,cur++这种操作实际是“读—改—写”三步,两个goroutine同时进来,可能都读到旧值,各自+1后再写回,结果只加了一次。现象是计数偏少、map panic报fatal error: concurrent map writes,或每次运行结果不一致。
sync.Mutex不是万能解药,用错反而更危险
常见误用包括:把mu定义成局部变量(每次调用新建一把锁)、在handler里锁住整个函数体(阻塞其他请求)、忘记用defer mu.Unlock()导致死锁。真正该锁的只有共享变量被读写的那几行,比如更新计数器或写map的键值对。
- 锁变量必须是包级或结构体字段,不能是函数内声明的
-
mu.Lock()后立刻跟defer mu.Unlock(),哪怕中间有return或panic也能释放 - 不要在锁内调用
http.Get、os.ReadFile等耗时操作,否则所有后续请求排队等锁 - 读操作也要加锁——只要存在并发写,读不加锁就可能读到脏数据或崩溃
sync.RWMutex只在读远多于写时才值得换
它比sync.Mutex多了读计数和唤醒逻辑,本身开销更大。如果你的场景是每秒几十次读、几分钟一次写(比如配置缓存),RWMutex能明显提升吞吐;但如果是高频状态更新(如实时在线人数),用它反而更慢,且容易漏掉RUnlock()导致goroutine泄漏——go run -race还检测不到这种问题。
- 读操作用
rw.RLock()+defer rw.RUnlock() - 写操作仍用
rw.Lock()+defer rw.Unlock(),和Mutex一样阻塞全部读写 - 绝不尝试“先RLock再升级为Lock”,Go不支持,必须先
RUnlock()再Lock()
channel比锁更符合Go哲学,但别滥用
用channel把共享变量的读写收口到一个专属goroutine里,天然避免竞争。比如启动一个stateManager goroutine,所有handler通过channel发IncRequest或GetRequest消息,它串行处理并回传结果。这种方式清晰、易测、无锁争用。
- 适合状态变更逻辑复杂、或需原子性组合多个变量修改的场景
- 缺点是引入额外goroutine和channel通信开销,QPS极高时延迟略升
- 别为了用channel而用:如果只是简单计数,
sync/atomic.AddInt64比channel和Mutex都轻量 - 注意channel容量——无缓冲channel会阻塞sender直到receiver就绪,有缓冲要防溢出
真正难的不是选哪个机制,而是判断哪段代码属于临界区、哪些变量必须绑定同一把锁、以及是否真的需要共享——很多时候把状态从全局挪进struct里,再用指针传参,比加锁更干净。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











