Go 服务里的超时与取消:把工作停在边界上

沿着一次请求的生命周期,区分等待超时、取消信号与持久任务,避免把后台工作交给页面存活时间。

文章目录

一次页面请求结束了,它发起的工作却不一定自然结束。数据库查询、外部调用和自行启动的 goroutine,都需要清楚的生命周期。

Go 的 context 提供了传递取消信号和截止时间的方式。使用它时,值得先想清楚一件事:这项工作到底属于当前请求,还是应该作为独立任务继续存在?

请求内的工作沿着请求传递

如果工作只是为了返回当前响应,就从请求的 context 出发,把它继续传给下游调用。一个常见的边界如下:

func loadSummary(parent context.Context, db *sql.DB) (int, error) {
    ctx, cancel := context.WithTimeout(parent, 2*time.Second)
    defer cancel()

    var total int
    err := db.QueryRowContext(ctx,
        "SELECT count(*) FROM contents").Scan(&total)
    return total, err
}

这里的两秒只是示例预算,需要按实际接口调整。派生 context 仍然继承父级取消;函数结束时调用 cancel,可以释放与它关联的资源。

不要因为下游函数需要一个 context,就在每一层重新创建 Background。那样会切断上层已经建立的取消关系。

信号需要接收方配合

context 不会自动停止任意计算。自行编写循环时,需要在合理的边界检查取消信号;调用 I/O 时,也要确认所用接口能够接收并处理 context。

如果一个函数忽略信号,调用方即使结束等待,这项工作也可能继续占用资源。因此,设置超时后还应观察下游是否退出,而不只检查 HTTP 响应是否变快。

持久任务不应依赖页面存活

发布整站、处理视频和导入内容,可能比一次页面请求活得更久。把它们直接挂在请求 context 下,用户离开页面就可能中断任务;简单换成 Background,又会失去任务状态和停机管理。

本站把这类操作交给持久任务:请求负责验证并返回任务回执,工作进程读取固定输入、执行并保存结果。页面只负责查询,管理进程退出时则通过自己的生命周期通知工作协程收尾。

这是任务系统的设计选择,context 只承担其中的取消传递,不替代任务记录、租约、重试或恢复。

区分三种结果

正常完成、等待超时和业务失败,应该尽量保留不同含义。尤其是写入操作,客户端超时并不能证明服务端没有成功;继续操作前,还需要查询原请求或任务。

把截止时间设在明确的边界上,把取消传到真正做事的地方,再把长期工作交给独立任务管理,通常比到处增加超时常量更容易维护。

参考:Go 官方文章:Go Concurrency Patterns: Context。示例使用标准库 context 和 database/sql,完整文件还需导入 context、database/sql 和 time。

所属专题

关闭图片