goroutine 启动缓存更新时不能直接用 go updatecache(key),因为循环中 key 是复用变量,所有 goroutine 会共享最后一个值;正确做法是显式捕获当前 key 值或传参;并发更新同一 key 需用 sync.once 或 singleflight.group 避免重复请求。

goroutine 启动缓存更新时为什么不能直接用 go updateCache(key)
因为 updateCache 通常依赖闭包变量,比如在循环中启动多个 goroutine 更新不同 key,若直接写 go updateCache(key),所有 goroutine 实际共享同一个 key 变量(循环变量复用),最终可能全更新成最后一个 key 的值。这是 Go 中最常踩的坑之一。
正确做法是把当前迭代值显式传入 goroutine:
for _, key := range keys {
key := key // 创建新变量,捕获当前值
go func() {
updateCache(key)
}()
}
或者更清晰地传参:
for _, key := range keys {
go func(k string) {
updateCache(k)
}(key)
}
并发更新同一缓存 key 时如何避免重复请求
多个 goroutine 同时发现缓存失效、同时去后端拉数据,会造成“缓存击穿”和下游压力激增。需要引入单例加载机制,即首次请求触发加载,其余等待结果返回。
推荐用 sync.Once + map[string]*sync.Once 或更稳妥的 singleflight.Group(来自 golang.org/x/sync/singleflight):
-
singleflight.Group.Do(key, fn)自动合并相同key的并发调用,只执行一次fn,其余协程阻塞等待结果 - 注意:返回值必须是
(interface{}, error),需做类型断言 - 不要在
fn内部再调用Do相同 key,否则死锁
var group singleflight.Group
func getFromCacheOrLoad(key string) (string, error) {
v, err, _ := group.Do(key, func() (interface{}, error) {
data, err := fetchFromDB(key)
if err != nil {
return nil, err
}
setCache(key, data) // 写入缓存
return data, nil
})
if err != nil {
return "", err
}
return v.(string), nil
}
缓存更新失败后要不要重试?goroutine 怎么安全退出
goroutine 本身没有“超时自动销毁”机制,一旦启动就运行到结束或 panic。如果 updateCache 长时间卡住(如网络 hang 住),会持续占用 goroutine 和内存资源。
必须主动控制生命周期:
- 给 HTTP 请求加
context.WithTimeout,避免无限等待 - 用
select配合ctx.Done()检查上下文取消 - 失败时不盲目重试——重试应由上层调度决定,goroutine 本身只做一次尽力而为的更新
- 不要在 goroutine 里 recover panic 后静默吞掉错误,至少 log 记录
go func(key string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := fetchWithContext(ctx, key)
if err != nil {
log.Printf("cache update failed for %s: %v", key, err)
return
}
setCache(key, data)
}(key)
更新缓存时写入不一致:为什么 setCache 要考虑原子性
如果多个 goroutine 并发调用 setCache(key, value),而底层是 map + mutex,且写操作非原子(比如先删再设、或序列化+写文件),就可能出现中间态被读取——例如旧值刚删、新值还没写完,此时另一个 goroutine 查缓存得到空结果,又触发一轮重复更新。
关键点:
- 缓存写入必须是“全有或全无”,推荐用支持 CAS 或原子替换的存储(如 Redis 的
SET、Ristretto 的Set) - 如果用本地 map,
sync.Map的Store是原子的,但注意它不保证 value 构造过程的线程安全 - 避免在
setCache内做耗时操作(如 JSON 序列化),应提前完成,再原子写入
真正容易被忽略的是:缓存更新和业务逻辑的时序耦合。比如订单状态变更后立刻更新缓存,但若更新 goroutine 还没跑完,下游服务就读到了旧缓存——这种场景必须靠外部协调(如消息队列 + 最终一致性),而不是靠 goroutine 调度顺序来保证。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











