Go 服务的并发控制:别让 goroutine 成为隐形内存泄漏 一、goroutine 便宜但不是免费Go 的并发模型简单好用很多后端服务会自然地把耗时任务丢进 goroutine。问题是 goroutine 虽然比线程轻但仍然占内存、调度资源也会持有连接、上下文和闭包变量。没有上限的 goroutine在线上就是慢性泄漏。典型问题包括请求里开 goroutine 但不等待向无缓冲 channel 发送后没人接收后台任务没有监听 context批量处理时对每条数据都启动协程。场景风险控制方式批量调用下游瞬时并发过高worker pool 或 semaphore后台消费无法退出contextHTTP 请求IO 卡住timeoutchannel 通信发送阻塞明确关闭和缓冲二、用 errgroup 管住生命周期errgroup适合处理一组相关任务任何一个任务失败就取消其他任务所有任务结束后统一返回错误。它比手写sync.WaitGroup更适合请求链路。funcLoadDashboard(ctx context.Context,uidint64)(*Dashboard,error){g,ctx:errgroup.WithContext(ctx)result:Dashboard{}g.Go(func()error{profile,err:userClient.GetProfile(ctx,uid)iferr!nil{returnerr}result.Profileprofilereturnnil})g.Go(func()error{orders,err:orderClient.ListRecent(ctx,uid)iferr!nil{returnerr}result.Ordersordersreturnnil})iferr:g.Wait();err!nil{returnnil,err}returnresult,nil}业务复杂时应避免共享可变状态。更稳妥的写法是每个 goroutine 写局部变量Wait后再组装结果。三、批量任务必须有限流下面是一个带并发上限的批量处理示例。sem控制同时运行的任务数量errgroup管理错误和退出。并发数不是越大越好要结合下游 QPS、数据库连接池和机器 CPU 设定。funcSyncUsers(ctx context.Context,ids[]int64,concurrencyint)error{g,ctx:errgroup.WithContext(ctx)sem:make(chanstruct{},concurrency)for_,id:rangeids{id:idselect{casesem-struct{}{}:case-ctx.Done():returnctx.Err()}g.Go(func()error{deferfunc(){-sem}()returnsyncOneUser(ctx,id)})}returng.Wait()}如果任务数量很大更稳定的方案是固定 worker pool从 jobs channel 里消费任务。worker pool 适合长队列semaphore 适合中小批量并发调用。四、用 pprof 证明问题线上怀疑 goroutine 泄漏时不要只看日志。Go 服务应该接入 pprof并限制访问来源。看 goroutine 堆栈、heap、block profile通常能定位卡在哪个函数。import_net/http/pproffuncmain(){gofunc(){_http.ListenAndServe(127.0.0.1:6060,nil)}()runServer()}curlhttp://127.0.0.1:6060/debug/pprof/goroutine?debug2|head-80go tool pprof http://127.0.0.1:6060/debug/pprof/heap go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds30实践建议给所有批量并发代码设置上限所有跨服务调用设置超时所有后台 goroutine 监听 context并把 pprof 纳入服务模板。goroutine 的问题通常不是语法层面的而是生命周期没有设计能退出、有限流、可观测才算并发代码写完整。如果你正在学习云原生或搭建个人项目推荐试试腾讯云轻量应用服务器新用户专享优惠一键部署开发环境非常方便点击查看优惠