不能裸用 map[string]chan,因go原生map非并发安全,多goroutine读写会触发确定性崩溃;推荐sync.rwmutex+map(读多写少)或sync.map(仅限短连接、低频写场景)。
直接用普通 map 存储 channel 会引发并发写 panic,必须搭配同步机制。实战中推荐两种稳定方案:一种是用 sync.rwmutex + map[string]chan interface{},适合键集合相对固定、读多写少的场景;另一种是用 sync.map,但仅限于「连接 id → 响应通道」这类生命周期动态、写入频次低的映射关系。
为什么不能裸用 map[string]chan?
Go 的原生 map 非并发安全。多个 goroutine 同时调用 m[reqID] = ch 或 ch := m[reqID],尤其在 RPC 客户端并发发请求、服务端并发回包时,极易触发 fatal error: concurrent map writes。这不是偶发问题,而是确定性崩溃。
方案一:RWMutex 包裹普通 map(推荐多数场景)
适用于请求 ID 可预知、连接生命周期较长、且写操作(注册/注销 channel)可控的 RPC 客户端。
- 定义结构体:type ResponseMap struct { mu sync.RWMutex m map[string]chan *Response }
- 注册 channel:func (r *ResponseMap) Set(id string, ch chan *Response) { r.mu.Lock(); defer r.mu.Unlock(); r.m[id] = ch }
- 获取并删除:func (r *ResponseMap) GetAndDelete(id string) (chan *Response, bool) { r.mu.Lock(); defer r.mu.Unlock(); ch, ok := r.m[id]; if ok { delete(r.m, id) }; return ch, ok }
- 初始化时用
make(map[string]chan *Response),无需预估容量;读操作用RLock(),开销极小
方案二:sync.Map(慎用,只适合特定模式)
sync.Map 不是万能替代品。它适合存储“短连接、临时 ID、写入后几乎不更新”的响应通道,比如 HTTP 请求 ID 或单次 RPC 调用的 msgid。
- Store 和 Load 成对使用:respCh := make(chan *Response, 1); rpcMap.Store(msgID, respCh)
- 务必检查
ok:if ch, ok := rpcMap.Load(msgID); ok { select { case res := - 不要在
Range中遍历所有 channel 并尝试读取——这无法保证原子性,且可能漏收或阻塞 - 用完及时
Delete,避免内存泄漏;LoadAndDelete更安全
配合 context 实现超时与自动清理
单纯存 channel 不够,需防止 goroutine 泄漏。典型做法是:在发起异步调用时启动一个清理 goroutine。
- 创建带超时的 channel:ch := make(chan *Response, 1); timer := time.AfterFunc(timeout, func() { rpcMap.Delete(reqID) })
- 收到响应后停掉 timer:defer timer.Stop()
- 或更简洁地用
context.WithTimeout,把 cancel 函数和 reqID 绑定,在 LoadAndDelete 后显式调用 cancel
不复杂但容易忽略:channel 缓冲区大小设为 1 即可,避免阻塞 sender;所有写 channel 操作必须有兜底(如 select default),防止因 reader 未就绪导致 hang 住整个调用链。











