Go中TCP通信需注意:服务器用net.Listen监听并循环Accept+goroutine处理连接;客户端用Dialer设超时避免阻塞;Read需处理部分读取和关闭情况;关闭连接要先写后关,避免broken pipe。

TCP 客户端与服务器在 Go 中实现起来非常直接,但容易在连接管理、读写阻塞和错误处理上出问题——关键不是“能不能连上”,而是“连上后怎么稳住”。
如何用 net.Listen 启动一个基础 TCP 服务器
Go 的 net.Listen 返回一个 net.Listener,它本身不处理连接逻辑,只负责接收新连接。常见误区是以为调用一次就能服务多个客户端,其实必须显式循环 Accept()。
- 监听地址格式必须带端口,例如
"localhost:8080"或":8080";写成"8080"会报错listen tcp: address 8080: missing port in address - 务必用
defer ln.Close(),否则端口可能被占用无法重启 - 每个
conn都要单独 goroutine 处理,否则第二个连接会被第一个阻塞住
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
log.Println("Accept error:", err)
continue // 别 panic,继续收下一个连接
}
go handleConnection(conn) // 每个连接一个 goroutine
}
如何用 net.Dial 建立 TCP 客户端并避免阻塞卡死
net.Dial 默认是同步阻塞的:如果目标地址不可达或防火墙拦截,它可能卡住几十秒才超时。生产环境必须控制连接建立耗时。
- 用
net.DialTimeout或更推荐的net.Dialer+Context控制超时 - 不要对
conn.Write或conn.Read不设限地读写大块数据,容易因对方未响应而永久阻塞 - 客户端也要显式
conn.Close(),否则文件描述符泄漏(尤其在压测时快速建连断连)
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
conn, err := dialer.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal(err) // 如:dial tcp [::1]:8080: connect: connection refused
}
defer conn.Close()
为什么 conn.Read 有时只读到部分数据,甚至返回 0
TCP 是字节流协议,Read 不保证一次把应用层“一条消息”读完。它只按内核缓冲区当前可用字节数返回,可能是 1 字节,也可能是 1KB,也可能返回 n == 0, err == nil(对端关闭连接)或 io.EOF。
立即学习“go语言免费学习笔记(深入)”;
- 永远检查
n > 0才处理数据,忽略n == 0的情况可能导致空循环 - 不要依赖
Read自动分包;自己定义协议(如头部4字节表示长度,或用换行符分隔) - 若需完整读取,用
io.ReadFull(要求精确字节数)或封装成bufio.Reader配合ReadString('\n')
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if n == 0 && err == nil {
log.Println("remote closed")
return
}
if err != nil && err != io.EOF {
log.Println("read error:", err)
return
}
data := buf[:n] // 只取真实读到的部分如何安全关闭连接并避免 write: broken pipe
这个错误通常发生在:服务器已关闭连接(或进程退出),客户端还在往已关闭的 conn 写数据。根本原因是 TCP 连接是全双工的,但关闭操作可以半开——Close() 会同时关闭读写,而 Shutdown() 可以只关写端。
- 服务器在处理完请求后,应先
conn.Write响应,再conn.Close();不要一边写一边关 - 客户端发完请求后想等响应,就别急着
Close(),应先Read到 EOF 或超时再关 - 若需优雅通知对端“我不再发了但还能收”,用
conn.(*net.TCPConn).CloseWrite()(注意类型断言)
最常被忽略的一点:net.Conn 关闭后,底层文件描述符释放需要时间,短时间内复用相同本地端口可能触发 bind: address already in use——加 SO_REUSEADDR(Go 默认已设)或换端口更稳妥。











