go 的 net.listen 默认不启用 so_reuseaddr,需用 net.listenconfig 手动设置;孤儿连接需 tcp keepalive 或应用层心跳探测;listener.close() 不关闭已建立连接,应配合 context 优雅关闭;http server 应优先使用 shutdown 而非 close。

Go 的 net.Listen 默认不开启地址重用,SO_REUSEADDR 需手动配置
Go 标准库的 net.Listen(比如 net.Listen("tcp", ":8080"))底层调用系统 socket 时默认不设 SO_REUSEADDR。这意味着进程崩溃或快速重启后,端口可能卡在 TIME_WAIT 状态,新服务启动直接报 address already in use。
解决方法是绕过 net.Listen,改用 net.ListenConfig 手动控制 socket 选项:
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
syscall.SetsockoptInt32(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
})
},
}
ln, err := lc.Listen(context.Background(), "tcp", ":8080")
- 仅 Linux/macOS 有效;Windows 上
SO_REUSEADDR行为略有不同,但通常也够用 - 注意
Control函数只在监听 socket 创建时执行一次,不能用于已建立连接 - 别在
Control里做耗时操作,会阻塞Listen返回
孤儿连接(orphaned connection)不是 Go 能自动清理的,得靠 TCP keepalive 或应用层心跳
所谓“孤儿连接”,是指客户端断网、崩溃或静默退出后,服务端仍维持着 ESTABLISHED 状态的连接。Go 的 net.Conn 不会主动探测对端是否存活——它只管读写,不负责判断“对方还在不在”。
依赖系统级 TCP keepalive 是最轻量的方案,但默认间隔太长(Linux 通常 2 小时),需显式开启并调小:
立即学习“go语言免费学习笔记(深入)”;
conn.(*net.TCPConn).SetKeepAlive(true) conn.(*net.TCPConn).SetKeepAlivePeriod(30 * time.Second)
- 必须在
Accept后对每个net.Conn单独设置,ListenConfig控制不了已建立连接 - keepalive 只能发现“对端彻底失联”,对卡住的中间网络(如 NAT 超时)效果有限
- 高可靠场景建议加应用层心跳(例如每 15 秒发个空帧 + 超时未响应则 close),否则容易积压数百个假连接
net.Listener.Close() 不等于连接全断,要等 Accept 循环退出 + 连接显式关闭
调用 listener.Close() 只会让后续 Accept() 返回错误(通常是 use of closed network connection),但已 Accept 出来的连接依然活着,也不会被自动中断。
常见误操作是:关 listener 后直接 exit,导致正在处理的连接被粗暴终止(没 flush、没 cleanup)。正确做法是配合 context 控制生命周期:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
<p>go func() {
for {
conn, err := ln.Accept()
if err != nil {
if !errors.Is(err, net.ErrClosed) {
log.Println("accept error:", err)
}
return
}
go handleConn(ctx, conn) // 传入带超时的 ctx
}
}()
-
handleConn内部要用conn.SetReadDeadline或ctx.Done()做读超时和取消感知 - 别依赖
defer conn.Close()就万事大吉——如果连接卡死在 read/write,它根本不会执行到 defer -
goroutine 泄漏比连接泄漏更隐蔽,务必确保每个
go handleConn都有明确退出路径
HTTP Server 的 Shutdown 和 Close 行为差异极大
如果你用的是 http.Server,别直接调 ln.Close(),而应走标准 shutdown 流程:server.Shutdown() 会尝试优雅关闭,server.Close() 是暴力关闭。
Shutdown 的关键点在于它会等待所有活跃请求结束(最多阻塞你给的 timeout),但前提是你的 handler 必须支持 context 取消:
srv := &http.Server{Addr: ":8080", Handler: myHandler}
go srv.ListenAndServe()
<p>// 关闭时:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx) // 会等正在处理的 handler 退出
- 如果 handler 里用了
time.Sleep或阻塞 I/O 且没检查ctx.Done(),Shutdown就会卡满 timeout 时间 -
http.Serve底层其实也是封装了net.Listener,但它把 accept 循环藏起来了,所以你没法像裸 TCP 那样精细控制每个连接 - 若 handler 中启了 goroutine 且没传入 context,
Shutdown完全无法感知,那些 goroutine 就成了真正的孤儿
实际部署中,SO_REUSEADDR 和 keepalive 是基础项,但最难缠的永远是应用层连接生命周期管理——尤其是跨 goroutine 的状态同步和超时传递,稍不留神就变成连接数缓慢上涨的“慢性病”。











