net.conn并发写会panic或数据错乱,因其接口不承诺并发安全,底层tcpconn无读写锁保护;多goroutine同时write竞争socket缓冲区和tls状态,导致粘包、截断、broken pipe等错误。

net.Conn 并发写为什么直接 panic 或数据错乱
因为 net.Conn 接口不承诺并发安全,底层实现(如 tcpConn)对读写操作无锁保护。多个 goroutine 同时调用同一个 conn.Write(),会竞争 socket 内核缓冲区和 TLS 记录层状态,导致:
- 响应体粘包或截断(比如两个 JSON 对象拼成
{"a":1}{"b":2}) - 随机出现
write: broken pipe或use of closed network connection - 启用
go run -race时明确报出写竞争
用 channel 串行 writer 是最简可靠的方案
核心是把所有写请求收拢到一个 goroutine 中顺序执行,避免锁、不阻塞业务逻辑、也不依赖底层实现细节。
- 定义请求结构体:
type writeReq struct { data []byte; resp chan error } - channel 建议带缓冲(如
make(chan writeReq, 128)),防 writer 暂堵时发送方阻塞过久 - 编码、序列化等耗时操作必须在发送方完成,
writer只做conn.Write() - 连接关闭前必须
close(ch)并sync.WaitGroup等 writer 退出,否则 goroutine 泄漏
为什么别给 conn.Write 包 sync.Mutex
加锁看似能解决并发写问题,但掩盖了更危险的竞态场景:
-
Mutex无法防止其他 goroutine 同时调用conn.Close()和Write(),Close 仍可能中断阻塞中的 Write,返回use of closed network connection - 若只锁 buffer 拼接逻辑,底层 socket 写调用仍并发,错乱照旧
- 高并发下锁争用会让吞吐量断崖式下跌,尤其在 TLS 连接中更明显
sql.DB 可以跨协程共享,但别和 net.Conn 混淆
*sql.DB 是连接池句柄,设计上就是并发安全的,可被任意数量 goroutine 直接调用 Query/Exec;而 net.Conn 是单次连接抽象,本质是操作系统 socket 文件描述符,天然非线程安全。
- 常见误操作:把
sql.DB当作需要加锁保护的“连接对象”,反而引入不必要开销 - 真正要小心的是
net.Conn、*os.File、bufio.Writer这类底层 I/O 句柄 - 它们的共享模式完全不同:一个靠池化+内部同步,一个靠 channel 串行化











