Golang TCP服务器defer未执行引发内存泄漏问题求助
Golang TCP服务器压测后内存泄漏:goroutine因channel未关闭阻塞
问题描述
我运行一个Golang TCP服务器,核心处理逻辑如下:
- 接收连接后启动goroutine执行
Handle函数处理连接 Handle中启动新goroutineReadFromConn,从连接读取数据写入queuechannel- 同时调用
ProcessQueue函数,从queuechannel读取数据做业务处理
设计的正常流程:
- 客户端主动关闭连接:
ReadFromConn读取时触发错误返回,其defer代码会向donechannel写入数据,并关闭done和queuechannel;ProcessQueue检测到donechannel的信号后停止处理并返回 ProcessQueue提前完成决策:流程回到Handle函数,Handle返回时的defer会关闭连接,触发ReadFromConn读取错误,进而关闭相关channel
异常现象:
压测后服务器内存占用无法回落,查看日志发现部分连接只打印了ERROR READING FROM CONN日志,却没有READ_FROM_CONN DEFER日志,推测defer未执行。这导致ProcessQueue因queue/done channel未关闭而永久阻塞,同时Handle的CLOSING...日志也缺失。判断是这些阻塞的goroutine导致内存无法释放,问题非必现,难以定位根因,附上简化版代码求助。
简化版代码
package main import ( "bufio" "log" "net" ) func Handle(conn net.Conn) { defer func() { log.Println("CLOSING...") conn.Close() }() queue := make(chan []byte) done := make(chan struct{}) go ReadFromConn(conn, queue, done) result := ProcessQueue(queue, done) log.Printf("Process result: %v", result) } func ReadFromConn(conn net.Conn, queue chan<- []byte, done chan<- struct{}) { defer func() { log.Println("READ_FROM_CONN DEFER") done <- struct{}{} close(done) close(queue) }() scanner := bufio.NewScanner(conn) for scanner.Scan() { data := scanner.Bytes() queue <- data } if err := scanner.Err(); err != nil { log.Printf("ERROR READING FROM CONN: %v", err) } } func ProcessQueue(queue <-chan []byte, done <-chan struct{}) interface{} { var dataList [][]byte for { select { case data := <-queue: dataList = append(dataList, data) // 模拟:当数据足够时提前返回 if len(dataList) >= 10 { return len(dataList) } case <-done: return len(dataList) } } } func main() { listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalf("Listen failed: %v", err) } defer listener.Close() for { conn, err := listener.Accept() if err != nil { log.Printf("Accept failed: %v", err) continue } go Handle(conn) } }
根因分析
defer未执行的核心场景
- channel发送阻塞:当
ProcessQueue提前返回后,Handle的defer关闭连接,但ReadFromConn可能正阻塞在queue <- data(此时ProcessQueue已退出,无人消费queue),导致scanner.Scan()的错误处理逻辑永远无法执行,defer也不会触发。 - 未捕获的panic:若连接遭遇强制中断(如客户端发RST),底层
conn.Read()可能触发panic,若ReadFromConn未做recover,goroutine会直接退出,defer代码不会执行。
- channel发送阻塞:当
goroutine阻塞连锁反应
ReadFromConn阻塞在queue发送时,Handle的defer关闭连接后,scanner.Scan()的错误要等到channel发送解除阻塞才会触发,但此时已无消费者,导致永久阻塞。ProcessQueue因done/queue未关闭,会一直阻塞在select分支,无法退出。
修复方案
1. 给queue设置缓冲区,避免发送阻塞
根据业务场景设置合适的缓冲区大小,防止ReadFromConn因无人消费而永久阻塞:
queue := make(chan []byte, 10) // 缓冲区大小按需调整
2. 在ReadFromConn中添加recover,确保defer执行
捕获可能的panic,保证defer代码能触发并清理资源:
func ReadFromConn(conn net.Conn, queue chan<- []byte, done chan<- struct{}) { defer func() { log.Println("READ_FROM_CONN DEFER") close(done) close(queue) }() // 捕获panic,避免goroutine直接退出 defer func() { if r := recover(); r != nil { log.Printf("ReadFromConn panic recovered: %v", r) } }() scanner := bufio.NewScanner(conn) for scanner.Scan() { data := scanner.Bytes() // 监听done信号,提前退出避免阻塞 select { case queue <- data: case <-done: return } } if err := scanner.Err(); err != nil { log.Printf("ERROR READING FROM CONN: %v", err) } }
3. ProcessQueue提前返回时主动关闭done
当ProcessQueue因数据足够提前返回,主动关闭done通知ReadFromConn停止读取:
func ProcessQueue(queue <-chan []byte, done chan<- struct{}) interface{} { var dataList [][]byte for { select { case data := <-queue: dataList = append(dataList, data) if len(dataList) >= 10 { close(done) // 主动关闭done,通知ReadFromConn退出 return len(dataList) } case <-done: return len(dataList) } } }
4. Handle中添加兜底的channel关闭逻辑
在Handle退出前主动关闭channel,避免因异常场景导致资源泄漏:
func Handle(conn net.Conn) { defer func() { log.Println("CLOSING...") conn.Close() }() queue := make(chan []byte, 10) done := make(chan struct{}) // 兜底关闭channel,确保goroutine能退出 defer func() { close(done) close(queue) }() go ReadFromConn(conn, queue, done) result := ProcessQueue(queue, done) log.Printf("Process result: %v", result) }
验证建议
- 压测时使用
pprof分析goroutine状态:go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2,查看阻塞的goroutine栈帧,确认是否为上述场景。 - 在
queue发送/接收节点添加更详细的日志,辅助定位阻塞点。
内容的提问来源于stack exchange,提问作者lHumanizado
相关产品推荐
相关产品推荐

