在Golang的Web开发中,使用context包能够有效控制请求的生命周期,尤其是在面对慢调用或下游服务异常时,及时中断请求可以避免资源堆积。context.WithTimeout和context.WithDeadline是控制超时的两个主要方式,它们返回一个带取消能力的子context和取消函数。

为什么需要context控制超时
如果不设置超时,一个阻塞的数据库查询或外部HTTP调用可能持续占用连接和协程,进而拖垮整个服务。context提供了一种统一的取消机制,让超时信号可以从入口层一直传递到最底层的IO调用。
在HTTP中间件中统一设置超时
最常见的方式是写一个中间件,在请求进入业务逻辑前生成带超时的context,并绑定到request上。
package main
import (
"context"
"net/http"
"time"
)
// TimeoutMiddleware 为所有请求设置超时
func TimeoutMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 创建2秒超时的context
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
// 将带超时的ctx放入request
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(3 * time.Second):
w.Write([]byte("done"))
case <-r.Context().Done():
// 超时后context被取消
http.Error(w, "timeout", http.StatusGatewayTimeout)
}
})
handler := TimeoutMiddleware(mux)
http.ListenAndServe(":8080", handler)
}
在调用链中传递超时
当handler中需要访问数据库或调用其他服务时,应直接使用request中的context,而不是自行创建背景context。这样超时信号才能贯穿整个调用链。
- 数据库查询使用
db.QueryContext(ctx, ...) - HTTP客户端使用
http.NewRequestWithContext(ctx, ...) - 自定义函数将context作为第一个参数传入
HTTP客户端示例
func callOtherService(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:9000/info", nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 简单读取处理
buf := make([]byte, 100)
n, _ := resp.Body.Read(buf)
return string(buf[:n]), nil
}
超时与取消的注意事项
使用context.WithTimeout时一定要调用返回的cancel函数,通常用defer完成,防止context泄漏。另外,超时后ctx.Err()会返回context.DeadlineExceeded,业务代码应据此做优雅降级。
| 场景 | 推荐做法 |
|---|---|
| Web入口 | 中间件统一设置WithTimeout |
| 内部调用 | 透传request的Context |
| 后台任务 | 使用WithTimeout但避免绑定用户请求 |
总结
通过context控制Web请求超时是Golang服务的标准做法。将超时集中在中间件处理,并在所有IO操作中传递同一个context,可以显著提升系统的容错能力和可维护性。