自定义并发队列引发index out of range等Panic问题求助
匹配服务器Panic问题排查与修复
问题1:索引越界(index out of range)Panic
原因
handleFindPeer中启动的goroutine里,queue.Len() > 0和queue.Dequeue()是两个独立操作,没有原子性保障。当多个请求并发时,可能出现多个goroutine同时检测到队列非空,第一个goroutine执行Dequeue取走最后一个元素,后续goroutine再执行Dequeue时队列已空,触发索引越界。
修复方案
给ClientQueue新增TryDequeue方法,在锁内部完成「检查队列长度+取出元素」的原子操作:
// client.go 中新增方法 func (q *ClientQueue) TryDequeue() (Client, bool) { q.mu.Lock() defer q.mu.Unlock() if len(q.clients) == 0 { return Client{}, false } client := q.clients[0] q.clients = q.clients[1:] return client, true }
修改handleFindPeer中的goroutine逻辑,用TryDequeue替代原有的判断+Dequeue:
go func() { peer, ok := queue.TryDequeue() if ok { sendPairingResponse(client, peer) close(client.peerMatchedCh) close(peer.peerMatchedCh) log.Printf("Paired %v and %v\n", client.addr, peer.addr) } else { queue.Enqueue(client) // 监听上下文取消,超时后从队列移除当前客户端 select { case <-ctx.Done(): queue.Remove(client) log.Printf("Removed timed out client %v from queue\n", client.addr) case <-client.peerMatchedCh: // 配对成功,无需移除 } } }()
问题2:WriteHeader called after Handler finished Panic
原因
当客户端等待配对超时后,handleFindPeer中的select会进入ctx.Done()分支,调用http.Error写入响应,此时Handler已结束。但如果该客户端仍在队列中,后续被取出配对时,sendPairingResponse会再次尝试写入响应,导致操作已关闭的响应writer,触发panic。同时,超时客户端未被从队列移除,会导致无效客户端残留。
修复方案
- 给
Client结构体添加sync.Once,确保每个响应只被写入一次:
// client.go 修改Client结构体 type Client struct { addr string respWriter http.ResponseWriter peerMatchedCh chan struct{} writeOnce sync.Once // 新增,保护响应写入操作 }
- 修改
sendPairingResponse,用writeOnce确保响应只写入一次:
// main.go 修改sendPairingResponse func sendPairingResponse(c1, c2 Client) { c1.writeOnce.Do(func() { c1.respWriter.WriteHeader(http.StatusOK) _, _ = c1.respWriter.Write([]byte(c2.addr)) // 强制刷新响应,避免客户端等待 if flusher, ok := c1.respWriter.(http.Flusher); ok { flusher.Flush() } }) c2.writeOnce.Do(func() { c2.respWriter.WriteHeader(http.StatusOK) _, _ = c2.respWriter.Write([]byte(c1.addr)) if flusher, ok := c2.respWriter.(http.Flusher); ok { flusher.Flush() } }) }
- 修改超时分支的响应写入,同样用
writeOnce保护:
// main.go 修改handleFindPeer的select分支 select { case <-client.peerMatchedCh: return case <-ctx.Done(): client.writeOnce.Do(func() { http.Error(w, "timeout waiting for peer", http.StatusRequestTimeout) }) }
额外优化点
ClientQueue.Remove方法中,用地址匹配替代addr匹配更可靠(不同客户端可能出现同一addr的情况,比如短连接复用端口),可以给Client改为指针存储:
// 修改ClientQueue为存储指针 type ClientQueue struct { clients []*Client mu sync.Mutex } // 对应修改Enqueue、TryDequeue、Remove等方法 func (q *ClientQueue) Enqueue(c *Client) { q.mu.Lock() defer q.mu.Unlock() q.clients = append(q.clients, c) } func (q *ClientQueue) TryDequeue() (*Client, bool) { q.mu.Lock() defer q.mu.Unlock() if len(q.clients) == 0 { return nil, false } client := q.clients[0] q.clients = q.clients[1:] return client, true } func (q *ClientQueue) Remove(client *Client) { q.mu.Lock() defer q.mu.Unlock() for i, c := range q.clients { if c == client { q.clients = append(q.clients[:i], q.clients[i+1:]...) return } } }
对应的handleFindPeer中创建客户端时改为指针:
client := &Client{ addr: r.RemoteAddr, respWriter: w, peerMatchedCh: make(chan struct{}), }
内容的提问来源于stack exchange,提问作者Diwas
相关产品推荐
相关产品推荐

