Go语言channel在什么情况下会引起资源泄漏?"

admin 2026-10-24 00:40:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Go 语言 channel 在什么情况下会引起资源泄漏?

原创

go
go

Go语言教程

2026年6月25日 13:21
陕西

在小说阅读器读本章

去阅读

线上 goroutine 从 180 涨到 64000,CPU 没爆,内存也没立刻爆,接口就是越来越慢。

pprof 一抓,堆栈很扎眼:

goroutine 58231 [chan send]:
order/internal/worker.pushResult(...)
    /app/worker/checker.go:47

goroutine 58232 [chan receive]:
order/internal/worker.start(...)
    /app/worker/checker.go:83

这种我第一眼一般不先看 GC,也不先怀疑数据库。Go 里看到 [chan send]、[chan receive] 长时间堆着,八成是 channel 把 goroutine 挂死了。

channel 本身不是洪水猛兽,真正泄漏的通常是 goroutine。

一个 goroutine 卡在 channel 上,只要它不退出,它手里引用的对象、上下文、连接、buffer,就都可能跟着活着。表面看是 channel 问题,实际是退出路径没设计好。

最常见的是发送没人接。

比如有个订单校验任务,后台并发查几个规则,谁先返回就用谁的结果。代码一开始可能这么写:

func checkOrder(orderID string) bool {
 resultCh := make(chan bool)

 go func() {
  ok := checkRisk(orderID)
  resultCh <- ok
 }()

 select {
 case ok := <-resultCh:
  return ok
 case <-time.After(200 * time.Millisecond):
  return false
 }
}

这段代码看着没毛病,超时返回,挺干净。

但问题在这里:一旦 checkRisk 超过 200ms,外层函数已经走了,resultCh 没人接了。后面的 resultCh <- ok 会一直卡住。

请求量一大,goroutine 就开始堆。

我一般会先加一行监控日志,不用上来就搞复杂工具:

go func() {
 ticker := time.NewTicker(10 * time.Second)
 defer ticker.Stop()

 for range ticker.C {
  log.Printf(\"goroutines=%d\", runtime.NumGoroutine())
 }
}()

如果这个数字只涨不降,就别安慰自己“业务高峰正常”。正常高峰是会上下波动,不是一路爬坡。

这段代码改法也不复杂,关键是给发送方也留退出口:

func checkOrder(ctx context.Context, orderID string) bool {
 resultCh := make(chan bool, 1)

 go func() {
  ok := checkRisk(orderID)

  select {
  case resultCh <- ok:
  case <-ctx.Done():
   log.Printf(\"risk result dropped, order=%s\", orderID)
  }
 }()

 timer := time.NewTimer(200 * time.Millisecond)
 defer timer.Stop()

 select {
 case ok := <-resultCh:
  return ok
 case <-timer.C:
  return false
 case <-ctx.Done():
  return false
 }
}

这里我会把 resultCh 改成缓冲为 1。不是说所有 channel 都要加 buffer,而是这种“最多只返回一个结果”的场景,外层即使超时走了,子 goroutine 也能把结果塞进去然后退出。

但别滥用 buffer。buffer 不是垃圾桶,它只是把阻塞往后推了一点。

第二种泄漏,是接收方一直等。

func consume(jobCh <-chan Job) {
 for {
  job := <-jobCh
  handle(job)
 }
}

这段代码的问题是,如果上游不再发送了,又没有关闭 channel,这个 goroutine 会永远卡在 <-jobCh。

生产里经常藏在 worker 里。服务都要停了,worker 还挂着;任务调度已经取消了,消费协程还在等。

我更愿意写成这样:

func consume(ctx context.Context, jobCh <-chan Job) {
 for {
  select {
  case job, ok := <-jobCh:
   if !ok {
    log.Printf(\"job channel closed\")
    return
   }
   handle(job)

  case <-ctx.Done():
   log.Printf(\"consumer stopped: %v\", ctx.Err())
   return
  }
 }
}

注意这个 ok。channel 被关闭之后,接收方还能读到零值,不判断 ok,有时候会把空任务当真任务处理,这个坑我见过,不止一次。

第三种,是生产者开了一堆,没人负责收尸。

func dispatch(ids []string) <-chan Order {
 out := make(chan Order)

 for _, id := range ids {
  go func(orderID string) {
   order := loadOrder(orderID)
   out <- order
  }(id)
 }

 return out
}

调用方如果只读前 10 条,剩下那些 goroutine 全卡在 out <- order。

这种代码在线下测不出来,因为测试数据少,读得也完整。到了线上,分页、超时、用户取消请求一来,马上露馅。

我一般会把并发数、退出信号、关闭动作都写明白:

func dispatch(ctx context.Context, ids []string) <-chan Order {
 out := make(chan Order, 32)
 guard := make(chan struct{}, 8)

 var wg sync.WaitGroup

 for _, id := range ids {
  select {
  case <-ctx.Done():
   break
  case guard <- struct{}{}:
  }

  wg.Add(1)

  go func(orderID string) {
   defer wg.Done()
   defer func() { <-guard }()

   order := loadOrder(orderID)

   select {
   case out <- order:
   case <-ctx.Done():
    log.Printf(\"skip send order=%s\", orderID)
   }
  }(id)
 }

 go func() {
  wg.Wait()
  close(out)
 }()

 return out
}

这段不漂亮,但我放心。并发数量有上限,发送有退出口,所有 goroutine 结束后关闭 out。线上代码有时候就该写得笨一点,别追求那种一眼看着很高级的链式封装。

还有一个容易被忽略的点:time.After 放在循环里。

for {
 select {
 case item := <-ch:
  handle(item)
 case <-time.After(time.Second):
  log.Println(\"idle\")
 }
}

低频没事,高频循环里这么写,会不停创建 timer。虽然 timer 到期后会被回收,但在压力大的时候,这种临时对象会把内存和 GC 搞得很难看。

我更喜欢显式拿着 timer 或 ticker:

ticker := time.NewTicker(time.Second)
defer ticker.Stop()

for {
 select {
 case item, ok := <-ch:
  if !ok {
   return
  }
  handle(item)

 case <-ticker.C:
  log.Println(\"idle\")

 case <-ctx.Done():
  return
 }
}

查 channel 泄漏,不要只盯代码猜。

我一般顺序是这样:

curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=2 | grep -E \"chan send|chan receive\" -n

然后看是不是同一个业务栈重复出现。如果几万条都卡在同一行,别犹豫,那一行就是现场。

channel 最容易泄漏的情况就这几类:发送没人接,接收没人发,退出没监听,关闭没约定,循环里乱造 timer。

Go 不会替你决定 goroutine 什么时候该死。

这个事必须代码里写清楚。否则 channel 看着很优雅,线上挂起来也很安静,等你发现的时候,goroutine 已经堆成一堵墙了。


Go语言channel在什么情况下会引起资源泄漏?" 网络安全文章

Go语言channel在什么情况下会引起资源泄漏?"

本文分析Go语言channel导致goroutine资源泄漏的常见场景,包括发送方无人接收、接收方持续等待、生产者无退出机制等。通过实际代码案例展示问题现象,并提供使用缓冲channel、context上下文控制、监控goroutine数量等解决方案。强调必须明确设计goroutine退出路径,避免内存泄漏和性能下降。
AceData平台PythonSDK接入教程" 网络安全文章

AceData平台PythonSDK接入教程"

本文详细介绍AceDataCloud平台PythonSDK的安装配置与使用方法,涵盖同步/异步客户端调用ChatCompletions、图像生成等核心功能,提供完整的代码示例和错误处理机制,并说明SSE流式响应、类型校验等特性。
挖洞思路|CORS配置漏洞背后的信任陷阱,赏金猎人如何靠逻辑链破防?" 网络安全文章

挖洞思路|CORS配置漏洞背后的信任陷阱,赏金猎人如何靠逻辑链破防?"

本文深入分析CORS配置漏洞的两种实战场景:一是受信子域名存在XSS漏洞时攻击者可利用CORS信任关系窃取主站敏感数据;二是HTTPS主站将HTTP子域列入白名单导致安全链路被降维打击。文章强调需从整体信任网络角度审视CORS白名单中每个源的安全状况,为渗透测试人员提供了通过逻辑链挖掘高危漏洞的方法论。
评论:0   参与:  0