你有没有遇到过这种情况:
用户早就把页面关了,
你的服务器却还在疯狂跑 SQL。
CPU 90%,goroutine 堵死,
数据库连接池被占满,
你一边查日志,一边怀疑人生:
“人都走了,服务怎么还在干活?”
这类 Bug,比你想象中更危险
我见过一次真实事故:
一个导出接口,没有任何超时控制。
用户点了导出,
发现数据太大,
直接关闭页面走人。
但服务器这边:
一条 SQL,跑了10 分钟。
接着又有 10 个用户点导出:
• 数据库连接池耗尽
• 正常请求全部变慢
• 服务直接雪崩
这些请求没有业务价值
却在消耗最贵的资源。
不设超时、不支持取消,本质上就是给系统埋雷。
根本原因
你的代码大概率是这样的:
func GenerateReport() {
// 不管用户还在不在,我都要跑完
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
fmt.Println("Processing part", i)
}
fmt.Println("Report complete")
}
他的问题只有一个:只要被调用,就必须跑完
服务器在干什么?
在为一个已经离开的用户拼命工作。
Context 是什么?
你可以把context当成:
一根可以随时拉闸的电线。
它能告诉函数三件事:
• ⏰ 超时时间
• 取消信号
• 请求范围内的值
你可以理解为:
上游一旦说“停”,
下游所有函数都必须停。
这个“停”,可能来自:
• 用户关了浏览器
• 超时了
• 程序主动取消
改造只需要一步
func GenerateReport(ctx context.Context) {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return
default:
}
time.Sleep(time.Second)
fmt.Println("Processing", i)
}
}
核心就一句:
在干活前,先看看有没有人叫你停。
从 HTTP 请求天然支持 Context
func HandleReport(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // 用户关闭页面会自动 cancel
GenerateReport(ctx)
}
现在流程变成:
用户关页面
Context 取消
goroutine 退出
资源释放
我亲自测了一下:
请求跑到一半关闭页面,
后面的逻辑根本不会执行
⏱️ 还不够:必须加「超时保护」
但只靠用户行为是不够的。
如果:
• 用户一直开着页面
• SQL 卡死
• 程序逻辑异常
你的服务还是会被拖死。
所以要加这一层保险:
func HandleReport(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
GenerateReport(ctx)
}
这段代码的含义是:
不管用户走没走,
5 秒后我强制拉闸。
这是服务器给自己的“保险丝”。
一个90%的人都会踩的坑:忘记 cancel()
这一句非常重要:
defer cancel()
很多人会问:超时不是会自动 cancel 吗?
是的,但问题是:WithTimeout 内部会创建定时器。
如果你的函数提前返回:
• ⏳ 定时器还在
• 🧠 资源还没释放
• 内存会慢慢积累
忘了 cancel = 定时器泄露。
这是非常隐蔽的一种内存泄漏,
线上才会慢慢暴露。
🧭 避坑清单(建议收藏)
•Context 永远作为第一个参数
•不要把 Context 存进 struct
•WithTimeout 一定要defer cancel()
•用select监听ctx.Done()
•用ctx.Err()打日志区分原因
🧨 总结一句话
服务器最怕的不是慢,
而是为“已经走掉的用户”拼命干活。
用了 Context:
• 用户走了 停
• 超时到了 停
• 系统保护 停
你写的每一行代码,
都应该知道:
什么时候该继续,什么时候该停。
你线上项目里,用 Context 了吗?
有没有遇到过:
• 用户早走
• 服务还在跑
• 数据库被拖死
的情况?
评论区聊聊你的真实经历。