net.conn并发write会错乱或panic,因其无写锁;tls.conn读写可并发因有独立读写锁,但多write仍串行;udpconn的readfromudp/writetoudp虽线程安全,但应用层语义可能混乱。

net.Conn 不是跨协程安全的写操作句柄,直接并发调用 Write 必然出错;但 tls.Conn 的读写可并发,UDPConn 的 WriteToUDP 和 ReadFromUDP 也可并发——关键在区分“线程安全”和“语义安全”。
为什么 net.Conn 并发 Write 会崩溃或错乱
Go 标准库明确不保证 net.Conn 的写操作并发安全。底层 tcpConn 没有为 Write 加锁,多个 goroutine 同时调用会导致:
- 数据粘连或截断(如 A 写
"{a:1}"、B 写"{b:2}",对端收到"{a:1}{b:2"或更糟) - panic:常见于 TLS 连接中触发
crypto/tls.(*block).reserve竞态 - 错误信息如
write: broken pipe或use of closed network connection在非预期位置出现 - 启用
go run -race会直接报出Write竞态
tls.Conn 的并发读写为什么可以但仍有坑
tls.Conn 的 Read 和 Write 方法各自持有独立互斥锁(c.in.Lock() 和 c.out.Lock()),所以读写可真正并发。但这不等于“随便用”:
- 多个
Write调用仍会串行执行(共用c.out锁),高频小包写入吞吐受限 - Handshake 未完成时首次
Read/Write会隐式阻塞握手,若多 goroutine 同时触发,可能造成重复 handshake 或超时失败 - 若你在 writer goroutine 里用
bufio.Writer包装tls.Conn,必须确保Flush调用不被并发打断——否则bufio.Writer内部 buffer 仍会竞态 - 不要混用:一个 goroutine 用
conn.Write(),另一个用bufio.NewWriter(conn).Write(),这是未定义行为
怎么安全地复用一个 TCP 连接做高并发写
最可靠方案是把写请求串行化到单个 goroutine,用 channel 中转:
- 定义请求结构体:
type writeReq struct { data []byte; resp chan - 启动 writer goroutine:
go func() { for req := range ch { _, err := conn.Write(req.data); req.resp - 发送方提前完成序列化(如 JSON 编码),只传
[]byte,避免 writer 中耗时操作 - 连接关闭前必须
close(ch)并sync.WaitGroup.Wait(),否则 goroutine 泄漏 - 绝对不要用
sync.Mutex包一层conn.Write:它无法防止Close和Write的竞态,且锁粒度粗导致吞吐骤降
UDP 场景下并发 ReadFromUDP 是安全的,但业务上通常不该这么干
*net.UDPConn 的 ReadFromUDP 和 WriteToUDP 可以并发调用,操作系统保证 sendto/write 的原子性。但问题不在 panic,而在逻辑:
- 多个 goroutine 同时
ReadFromUDP,谁读到哪个客户端的数据包完全随机,无法绑定请求-响应 - 你没法知道某个
WriteToUDP是否已送达,也没法按源地址分发响应 - 真实项目几乎都采用“一个连接一个 goroutine +
for循环ReadFromUDP”,再用map[addr]chan把响应投递到对应处理流 - 若真要用并发 UDP I/O,应改用
net.PacketConn配合ReadFrom/WriteTo,并自行维护地址-上下文映射
真正容易被忽略的点是:很多人以为加了 mutex 就万事大吉,结果在线上压测时发现吞吐卡在 1k QPS 上不去——那不是锁的问题,是没意识到 Write 本身在 TLS 层要加密+分片+写底层 socket,串行化才是正解。别省那几行代码,channel + 单 writer 是 Go 生产环境事实标准。











