defer cancel()无效是因为长连接handler永不返回,cancel无法触发;必须在conn.read()返回io.eof或错误时显式调用cancel,并配合select { case

长连接 handler 里 defer cancel() 为什么无效
因为 HTTP handler 函数不返回,defer cancel() 就永远不会执行。长连接(如 SSE、WebSocket、gRPC stream)的 handler 通常进入无限循环,ctx, cancel := context.WithCancel(r.Context()) 后直接 defer cancel() 等同于“写了但没用”。
真正有效的 cancel 必须绑定到连接实际断开的那一刻——不是函数退出,而是底层 conn.Read() 返回 io.EOF 或网络错误时。
- 错误模式:
func sseHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithCancel(r.Context()); defer cancel(); serveSSE(ctx, w) }→serveSSE卡住,cancel永不调用 - 正确做法:在
serveSSE内部,每次conn.Read()或responseWriter.Write()出错后立即调用cancel() - 别在多个 goroutine 里重复调
cancel()—— 它幂等,但掩盖了“谁该负责关闭”的逻辑缺陷
阻塞读写必须用 select + ctx.Done(),不能只靠 SetReadDeadline
conn.SetReadDeadline() 只能防卡死,无法替代 context 的语义化取消。超时是时间维度控制;而 ctx.Done() 是事件驱动信号:客户端断连、服务端主动下线、配置热更新都会触发它。
裸用 deadline 容易漏掉真实断连:比如客户端进程崩溃,TCP 连接静默消失,Read() 可能长时间阻塞或返回 net.ErrClosed,但你不检查就继续循环,goroutine 和资源就滞留了。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 错误写法:
for { conn.SetReadDeadline(time.Now().Add(30 * time.Second)); n, _ := conn.Read(buf) }→ 忽略err,不判断是否为io.EOF或net.ErrClosed - 正确写法:
select { case ,且每次读完立刻检查 <code>err - gRPC stream 同理:
stream.Recv()必须配合select,不能裸调;stream.Context()已自带断连信号,别 wrap 新 context
DB 连接、TLS session、buffer pool 怎么清理
GC 不会帮你关 TCP 连接、释放 TLS handshake 状态、归还预分配内存块。这些都得在 ctx.Done() 分支里手动做。
- DB 连接:若用
*sql.Conn(来自db.Conn(ctx)),必须显式调.Close();sql.DB连接池本身不用关,但长连接中持有的单次连接要释放 - TLS session:若复用
tls.Conn并启用 session resumption,断连时建议丢弃整个tls.Conn实例,或至少清理conn.ConnectionState().PeerCertificates - 自定义 buffer pool:在
ctx.Done()分支中调pool.Put()归还块;若用sync.Pool,它不保证回收时机,必须自己管理生命周期
SSE 场景下全局 ReadTimeout/WriteTimeout 必须禁用
Go http.Server 的 ReadTimeout 和 WriteTimeout 是连接级计时器,从 TCP 建立或请求头接收开始算起。对 SSE 这种持续数分钟甚至小时的长连接,设 10s 超时等于主动掐断连接,前端频繁重连、数据丢失。
正确做法是彻底移除这两个字段,把超时控制收回到 handler 内部:靠心跳探测、写入间隔检测、以及 ctx.Done() 主动退出。
- 错误配置:
http.Server{ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second }→ SSE 连接 10 秒必断 - 正确配置:
http.Server{ Addr: ":8080", Handler: mux },不设任何 timeout 字段 - handler 内需自行保障:每次
Write()后 flush,定期发送event: heartbeat,并监听ctx.Done()清理资源
真正的难点不在“怎么写 cancel”,而在“cancel 由谁触发、何时触发、触发后是否所有依赖都收到通知”。一个未被 ctx.Done() 覆盖的阻塞点,就能让 goroutine 和资源永远卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










