conn.Read()卡住或只读部分数据是因TCP流式传输无消息边界,需用分隔符、固定头或自描述格式界定消息;推荐bufio.Scanner处理换行协议,io.ReadFull()处理二进制协议,并务必设置ReadDeadline防goroutine泄漏。

为什么 conn.Read() 会卡住或只读到部分数据
Go 的 net.Conn.Read() 是阻塞式、底层无消息边界的系统调用,它不保证一次读完应用层“一条完整消息”。常见现象是:客户端发了 "HELLO\n",服务端 Read() 却只拿到 "HE",下一次才读到 "LLO\n"。这不是 bug,而是 TCP 流式传输的天然特性。
解决思路不是反复调用 Read() 猜长度,而是明确界定消息边界。常用方式有三种:固定长度头 + 内容、分隔符(如 \n)、JSON/Protocol Buffers 等自描述格式。对简单调试或日志类场景,分隔符最轻量。
- 不要写
buf := make([]byte, 1024); n, _ := conn.Read(buf)就直接当完整字符串用 - 若用分隔符,必须循环读直到遇到
\n或超时,不能依赖单次Read() -
Read()返回n > 0仅表示读到了字节,不表示消息结束;返回n == 0 && err == nil是非法状态,可忽略
用 bufio.Scanner 安全读取带换行的消息
对以 \n 分割的文本协议(如 Telnet、简单控制指令),bufio.Scanner 是最简方案。它内部自动缓冲、切分,并处理常见边界情况(如超长行、空行)。
注意:默认最大扫描长度是 64KB,超长行会报 scanner: token too long。生产环境务必显式设置 Split() 和 BufSize。
立即学习“go语言免费学习笔记(深入)”;
scanner := bufio.NewScanner(conn) scanner.Split(bufio.ScanLines) // 明确按行切分 scanner.Buffer(make([]byte, 4096), 1<<20) // 缓冲区 4KB,最大行 1MBfor scanner.Scan() { line := scanner.Text() // 不含 \n fmt.Printf("Received: %s\n", line) if line == "quit" { break } } if err := scanner.Err(); err != nil { log.Println("Scan error:", err) }
手动处理粘包:用 io.ReadFull() 读固定头再读内容
当协议要求严格二进制格式(例如前 4 字节是 uint32 消息长度),就不能依赖换行。此时需分两步:先读够头部长度,再按头部指示读取正文。
io.ReadFull() 能确保读满指定字节数,避免手动循环判断 n 。但它会在连接关闭或出错时返回 io.ErrUnexpectedEOF,需区分处理。
- 头长度必须是固定字节数(如
binary.Write()写入的uint32是 4 字节) - 读头失败(如连接断开)应立即退出,不能继续读正文
- 正文长度过大时需加限制,防止内存耗尽(例如限制最大 10MB)
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
log.Println("Failed to read header:", err)
return
}
msgLen := binary.BigEndian.Uint32(header)
if msgLen > 1010241024 { // 10MB 上限
log.Println("Message too large")
return
}
body := make([]byte, msgLen)
if _, err := io.ReadFull(conn, body); err != nil {
log.Println("Failed to read body:", err)
return
}
// 处理 body
为什么 conn.SetReadDeadline() 不可少
没有读超时的 socket 服务,在客户端异常断连、网络中断或发送半截数据时,Read() 会永久阻塞,导致 goroutine 泄漏。Go 不会自动回收这些 goroutine。
必须在每次读操作前设置合理的读超时(例如 30 秒),并在超时后主动关闭连接。注意:超时是针对单次 Read(),不是整个连接生命周期。
- 不要只在
Accept()后设一次超时——后续多次Read()都需要重设 - 超时时间要大于业务预期最大响应间隔,但不能过长(如设 24 小时)
-
SetReadDeadline(time.Now().Add(30 * time.Second))必须在每次Read()前调用
粘包处理逻辑里,所有可能阻塞的读操作(包括 scanner.Scan()、io.ReadFull())都受此 deadline 约束。一旦超时,err 会是 *net.OpError 且 Timeout() == true。










