Go语言中forselect时,如果通道已经关闭会怎么样?如果只有一个case呢?"

admin 2026-10-24 02:05:59 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Go 语言中 for select 时,如果通道已经关闭会怎么样?如果只有一个 case 呢?

原创

go
go

Go语言教程

2026年6月24日 13:21
陕西

在小说阅读器读本章

去阅读

生产上看到这种日志,我第一眼就不太信业务代码:

worker=sync-order cost=0ms order_id=0
worker=sync-order cost=0ms order_id=0
worker=sync-order cost=0ms order_id=0
worker=sync-order cost=0ms order_id=0

CPU 还跟着涨,接口没多少流量,日志倒是刷得挺勤。

这种情况我一般先翻 for select,尤其是里面读 channel 的地方。Go 里 channel 一旦被关闭,接收方不是阻塞,也不是报错,而是立刻返回零值。

看这段代码:

type OrderJob struct {
 ID     int64
 UserID int64
}

func runOrderWorker(jobs <-chan OrderJob) {
for {
select {
case job := <-jobs:
   handleOrder(job)
  }
 }
}

func handleOrder(job OrderJob) {
 fmt.Printf(\"handle order, id=%d, user=%d\
\", job.ID, job.UserID)
}

这代码平时看不出问题。

jobs 里有数据,就消费。没数据,就卡在 select 那里等。

但只要外面执行了:

close(jobs)

问题就变味了。

case job := <-jobs 会一直就绪,每次都能读出来一个 OrderJob{}。也就是:

OrderJob{
 ID:     0,
 UserID: 0,
}

所以日志里才会一直出现 order_id=0。

这不是 Go 调度器抽风,也不是 select 随机选错了,是代码没判断 channel 是否关闭。

正确写法至少要带上 ok:

func runOrderWorker(jobs <-chan OrderJob) {
for {
select {
case job, ok := <-jobs:
   if !ok {
    fmt.Println(\"jobs channel closed, worker exit\")
    return
   }

   if job.ID <= 0 {
    fmt.Printf(\"skip dirty job: %+v\
\", job)
    continue
   }

   handleOrder(job)
  }
 }
}

ok=false 才是关键信号。

这个地方别偷懒。尤其 channel 里传的是 int、string、结构体这种类型,零值看起来很像正常数据。

比如 chan int64 关闭后读出来是 0。

比如 chan string 关闭后读出来是 \"\"。

比如 chan *OrderJob 关闭后读出来是 nil。

如果后面代码没兜住,就会变成各种奇怪问题。轻一点是日志刷屏,重一点是拿着空数据去查库、调接口、写脏记录。

我见过有人这么写:

func watchUserEvent(events <-chan int64) {
 for {
  select {
  case userID := <-events:
   refreshUserCache(userID)
  default:
   time.Sleep(20 * time.Millisecond)
  }
 }
}

他以为加了 default 就不会卡死。

但 channel 关闭后,case userID := <-events 永远是可执行状态,default 根本没机会走。于是 refreshUserCache(0) 被疯狂调用。

修的时候不要靠 sleep 压住,这种压法只能把事故从“马上炸”改成“慢慢烂”。

改成这样:

func watchUserEvent(events <-chan int64) {
for {
select {
case userID, ok := <-events:
   if !ok {
    fmt.Println(\"events channel closed, stop watcher\")
    return
   }

   if userID == 0 {
    fmt.Println(\"ignore empty user event\")
    continue
   }

   refreshUserCache(userID)

default:
   time.Sleep(20 * time.Millisecond)
  }
 }
}

再说一个更容易被忽略的点:多个 case 时,关闭的 channel 会一直参与 select。

比如:

func mergePayEvent(payCh <-chan string, refundCh <-chan string) {
 for {
  select {
  case pay := <-payCh:
   fmt.Println(\"pay:\", pay)

  case refund := <-refundCh:
   fmt.Println(\"refund:\", refund)
  }
 }
}

如果 payCh 被关闭了,refundCh 还没关闭,这个循环不会老老实实只等退款事件。

payCh 那个 case 会一直就绪,读出来空字符串。

最后你可能看到这种日志:

pay:
pay:
pay:
refund: R20240518001
pay:
pay:

这里我一般会把关闭的 channel 置成 nil。

因为 nil channel 在 select 里永远不会就绪,相当于把这个 case 临时摘掉。

func mergePayEvent(payCh <-chan string, refundCh <-chan string) {
for payCh != nil || refundCh != nil {
select {
case pay, ok := <-payCh:
   if !ok {
    fmt.Println(\"pay channel closed\")
    payCh = nil
    continue
   }
   fmt.Println(\"pay:\", pay)

case refund, ok := <-refundCh:
   if !ok {
    fmt.Println(\"refund channel closed\")
    refundCh = nil
    continue
   }
   fmt.Println(\"refund:\", refund)
  }
 }

 fmt.Println(\"all event channels closed\")
}

这段比到处加标志位舒服。

payCh = nil 之后,select 就不会再选它。等两个 channel 都变成 nil,外层循环退出。

这里要注意,不能写成死循环:

for {
 select {
 case v := <-ch:
  fmt.Println(v)
 }
}

如果只有这一个 case,情况更简单,也更坑。

channel 没关闭时,它会阻塞等数据。

channel 关闭后,它不会阻塞,会一直读零值,然后循环马上进入下一轮。

这段代码关闭后等价于一个空转机器:

for {
 v := <-closedCh
 fmt.Println(v)
}

打印慢一点还好,要是里面没 IO,只做计数、判断、调用本地函数,CPU 很快就能顶上去。

还有人问:只有一个 case,那还要不要 select?

多数时候不需要。

只有一个接收分支,直接 range 反而更干净:

func consumeAuditLog(logCh <-chan string) {
 for line := range logCh {
  if line == \"\" {
   continue
  }
  writeAuditFile(line)
 }

 fmt.Println(\"audit log channel closed\")
}

for range channel 会在 channel 关闭并且数据读完后自动退出。

这个写法适合单通道消费。少一个 select,少一个坑。

但如果你还要监听退出信号,就可以保留 select:

func consumeWithStop(logCh <-chan string, stopCh <-chan struct{}) {
for {
select {
case line, ok := <-logCh:
   if !ok {
    fmt.Println(\"log channel closed\")
    return
   }
   writeAuditFile(line)

case <-stopCh:
   fmt.Println(\"receive stop signal\")
   return
  }
 }
}

这里还有个反向坑:往已经关闭的 channel 发送数据会 panic。

func pushMetric(metricCh chan<- string, name string) {
 metricCh <- name
}

如果外面把 metricCh 关了,这里不是返回失败,也不是阻塞,是直接 panic:

panic: send on closed channel

所以 channel 的关闭权要收紧。

谁创建,谁负责关闭。多个发送方抢着 close,迟早出事。

我一般会把发送方收口到一个 goroutine,外面只发数据,不碰 close:

type MetricBus struct {
 ch   chanstring
 done chanstruct{}
}

func NewMetricBus() *MetricBus {
return &MetricBus{
  ch:   make(chanstring, 128),
  done: make(chanstruct{}),
 }
}

func (b *MetricBus) Push(name string) bool {
select {
case b.ch <- name:
returntrue
case <-b.done:
returnfalse
default:
  fmt.Printf(\"metric dropped: %s\
\", name)
returnfalse
 }
}

func (b *MetricBus) Close() {
close(b.done)
close(b.ch)
}

这段不是让你照搬,重点是别让一堆业务代码到处 close(ch)。

for select 里遇到关闭 channel,记住几个判断就够了:

读关闭的 channel,不阻塞,返回零值,ok=false。

关闭的 channel 在 select 里永远就绪。

只有一个 receive case 时,channel 一关,循环就可能空转。

多个 channel 时,某个关了,要么 return,要么置 nil 摘掉。

发送到关闭 channel,会 panic。

Go 的 channel 设计没问题,问题通常出在我们把“没数据”和“关闭了”混在一起看。线上排这种问题,我不会先怀疑 goroutine 泄漏,也不会先调调度参数,先看接收方有没有写 ok。这个地方漏一次,日志和 CPU 会帮你记住。


Go语言中forselect时,如果通道已经关闭会怎么样?如果只有一个case呢?" 网络安全文章

Go语言中forselect时,如果通道已经关闭会怎么样?如果只有一个case呢?"

本文详细分析了Go语言中for-select循环在通道关闭时的行为隐患及解决方案。当通道关闭后,接收操作会立即返回零值而非阻塞,导致循环空转、日志刷屏或CPU飙升。关键修复方案包括使用ok模式判断通道状态、将已关闭通道置为nil以禁用对应case、优先使用for-range处理单通道场景,并强调了发送到已关闭通道会panic的注意事项。
法国政府加密通讯平台Tchap遭黑客入侵官方宣称加密聊天记录未泄露" 网络安全文章

法国政府加密通讯平台Tchap遭黑客入侵官方宣称加密聊天记录未泄露"

法国政府加密通讯平台Tchap于2026年6月遭黑客通过社会工程攻击入侵,攻击者利用被劫持的合法账号访问平台,可能泄露用户分享的个人数据。官方确认加密聊天记录未泄露,但攻击者声称获取了大量文档、消息记录及用户信息,并指出平台存在无需令牌即可下载文件的架构缺陷。事件凸显政府通讯平台需加强账号安全管理和访问控制。
微软开源工具遭黑客入侵被用于窃取AI开发者密码" 网络安全文章

微软开源工具遭黑客入侵被用于窃取AI开发者密码"

2026年6月,微软GitHub上数十个开源项目遭黑客入侵,攻击者向Azure云服务及AI开发工具等组件注入恶意代码,旨在窃取开发者密码及敏感凭据。微软已紧急下线相关仓库并展开调查,部分仓库经复核后恢复。安全研究指出此为针对大型科技企业的软件供应链攻击,可能影响广泛开发者群体。
新型BitB钓鱼攻击针对Microsoft365登录" 网络安全文章

新型BitB钓鱼攻击针对Microsoft365登录"

一种新型浏览器内浏览器网络钓鱼攻击利用伪造的OAuth登录窗口窃取Microsoft365凭证,该攻击通过可定制化弹窗模拟真实系统对话框并具备反分析能力。研究发现此类攻击可绕过传统多因素认证,建议组织结合用户培训、条件访问策略及直接输入官网地址等方式进行防御。
评论:0   参与:  0