限流器选型取决于场景:rate.limiter适用于api入口层速率限制(如每秒100次),支持突发流量;semaphore.newweighted适用于任务级并发控制(如同时最多8个db查询),支持context取消和panic安全释放。

Go 微服务扛不住百万级并发,问题通常不出在语言本身,而在于限流没做对、goroutine 泄漏、下游没隔离。原生 golang.org/x/time/rate 能跑通 demo,但生产环境必须搭配信号量 + context + worker pool 才稳。
限流器选 rate.Limiter 还是 semaphore.NewWeighted?
rate.Limiter 适合请求入口层的速率限制(比如每秒最多 100 次 API 调用),它基于令牌桶,允许突发流量;semaphore.NewWeighted(Go 1.21+)更适合任务级并发控制(比如最多同时处理 8 个数据库查询),它本质是带权重的信号量,天然支持 context 取消和 panic 安全释放。
- 入口限流用
rate.NewLimiter(100, 200):每秒 100 令牌,桶容量 200,防毛刺流量打穿 - DB/HTTP 下游调用用
semaphore.NewWeighted(int64(8)):硬控 goroutine 数,避免耗尽连接池 - 别混用——
rate.Limiter不阻塞 goroutine,semaphore会阻塞,语义完全不同
goroutine 泄漏的三个典型场景
泄漏不是“忘了 go”,而是协程启动了却卡死或没回收。最常见于:
- channel 写入未配对读取:
ch 后没 goroutine 从 <code>ch接收,写操作永久阻塞 - HTTP handler 中启 goroutine 但没传
context:请求 cancel 后协程还在跑,http.Request.Context()没被监听 - worker pool 里任务 panic 且没 recover:
defer里的sem.Release(1)永远不执行,许可证永远卡住
worker pool 必须带超时与错误传播
裸用 for range tasks { go doWork(t) } 是高危操作。真实 worker pool 要封装 errgroup.Group 和 semaphore:
- 初始化:
g, ctx := errgroup.WithContext(context.WithTimeout(parentCtx, 5*time.Second)) - 设限:
g.SetLimit(16)(底层用semaphore.NewWeighted) - 提交任务:
g.Go(func() error { return doWork(ctx, task) }),任务内必须检查ctx.Err() - 等待:
err := g.Wait(),任一任务返回非 nil error 就终止,其他仍在运行,需靠ctx主动退出
为什么 GMP 调度器救不了无节制的 goroutine
Goroutine 轻量,但不是免费的。每个默认占 2KB 栈空间,10 万个就是 200MB 内存;调度器能 work-stealing,但无法阻止你把 10 万请求全塞进内存再并发——OOM 或 GC 频繁才是真瓶颈。
- 真正要做的不是“怎么多开 goroutine”,而是“怎么让 goroutine 少开、快退、不卡”
- 所有外部依赖(DB、HTTP、RPC)必须设超时,否则一个慢请求拖垮整个 pool
- 日志、metrics 上报这类副作用操作,别放主流程 goroutine 里,用独立 channel 异步批处理
复杂点不在代码行数,而在每个 goroutine 的生命周期是否可控——许可证是否归还、context 是否传递、panic 是否 recover。漏掉任意一环,压测时看着 QPS 很高,线上跑两天就内存持续上涨。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











