You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang TCP服务器defer未执行引发内存泄漏问题求助

Golang TCP服务器压测后内存泄漏:goroutine因channel未关闭阻塞

问题描述

我运行一个Golang TCP服务器,核心处理逻辑如下:

  • 接收连接后启动goroutine执行Handle函数处理连接
  • Handle中启动新goroutineReadFromConn,从连接读取数据写入queue channel
  • 同时调用ProcessQueue函数,从queue channel读取数据做业务处理

设计的正常流程:

  1. 客户端主动关闭连接:ReadFromConn读取时触发错误返回,其defer代码会向done channel写入数据,并关闭done和queue channel;ProcessQueue检测到done channel的信号后停止处理并返回
  2. 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)
	}
}

根因分析

  1. defer未执行的核心场景

    • channel发送阻塞:当ProcessQueue提前返回后,Handle的defer关闭连接,但ReadFromConn可能正阻塞在queue <- data(此时ProcessQueue已退出,无人消费queue),导致scanner.Scan()的错误处理逻辑永远无法执行,defer也不会触发。
    • 未捕获的panic:若连接遭遇强制中断(如客户端发RST),底层conn.Read()可能触发panic,若ReadFromConn未做recover,goroutine会直接退出,defer代码不会执行。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 08:37:17