Go TCP服务需用goroutine处理每个conn并设超时,避免阻塞和泄漏;TCP粘包须手动处理长度头或换行分隔;UDP需复用ReadFrom返回的addr回包,且不可跨goroutine共享。

Go 语言做 Socket 编程不需要额外库,net 包开箱即用;但直接操作 net.Conn 和 net.Listener 容易忽略连接生命周期、粘包、超时和错误复用等问题——这些才是实际写服务时卡住的关键。
怎么快速启动一个 TCP 服务端(不踩 Accept 阻塞和 goroutine 泄漏坑)
用 net.Listen 启动监听后,必须对每个 conn 启动独立 goroutine 处理,否则后续连接会被阻塞。但若不加控制地 go handle(conn),客户端异常断连时可能留下未回收的 goroutine。
-
Listen后立即用for循环调用Accept,不要试图“一次收多个连接” - 每个
conn进入 goroutine 前,建议设置SetDeadline或SetReadDeadline,避免读阻塞拖垮整个协程池 - 在
handle函数末尾务必conn.Close(),Go 不会自动回收底层 socket 文件描述符 - 示例片段:
ln, _ := net.Listen("tcp", ":8080") defer ln.Close() for { conn, err := ln.Accept() if err != nil { continue } // 如临时资源不足,不应退出循环 go func(c net.Conn) { defer c.Close() c.SetReadDeadline(time.Now().Add(10 * time.Second)) // ... 处理逻辑 }(conn) }
为什么 conn.Read 会读到不完整数据(粘包问题真实场景)
TCP 是字节流协议,Read 只保证最多读 len(buf) 字节,不保证一次读完一个“消息”。HTTP 或自定义协议中常见的换行分隔、长度前缀等,都得自己处理。
- 不要假设
Read返回非零 n 就等于收到一条完整请求;n == 0 且 err == nil 是合法的(如对端写一半就 close) - 简单协议可用
bufio.Scanner(适合行分隔),但注意MaxScanTokenSize默认仅 64KB,超长行会失败 - 二进制协议推荐手写长度头解析:先读固定 4 字节
binary.BigEndian.Uint32,再循环读满对应 body 长度 - 别在
Read后直接string(buf[:n])打印调试——未初始化的 buf 剩余部分可能是脏数据,应只取buf[:n]
UDP 场景下 WriteTo 和 ReadFrom 的地址复用陷阱
UDP 是无连接的,ReadFrom 返回的 addr 是发送方地址,必须原样传给 WriteTo 才能回包;如果存错或复用旧 addr,包会发到错误目标甚至本地丢弃。
-
WriteTo第二个参数不能是nil,也不能是监听地址(如:8080),必须是ReadFrom返回的具体net.Addr - 并发读写时,别把
addr存成包级变量或结构体字段并跨 goroutine 复用——它可能被下次ReadFrom覆盖(Go 标准库为性能复用底层内存) - 安全做法:每次
ReadFrom后立刻拷贝addr.String()或用addr.Network() + addr.String()构造新地址,或直接在同一个 goroutine 内完成读+写 - UDP 没有连接状态,所以没有
conn.Close()概念,只需关闭net.PacketConn即可释放端口
真正难的不是写出第一个 echo 服务,而是当 QPS 上千、连接频繁断连、网络延迟抖动时,SetDeadline 是否覆盖所有读写路径、conn.Read 的返回值是否被每处都正确检查、UDP 的 addr 是否在多 goroutine 下被意外共享——这些细节不写进日志,就只能靠抓包和压测暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











