实现超时控制
问题
如何在 Go 中实现各层的超时控制?
答案
超时控制的重要性
没有超时的系统就像没有保险丝的电路 — 一个慢服务可能拖垮整个链路(级联超时)。
Context 超时(核心机制)
// 方式一:WithTimeout(相对时间)
func CallWithTimeout() error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // 必须调用,释放资源
return doWork(ctx)
}
// 方式二:WithDeadline(绝对时间)
func CallWithDeadline() error {
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
return doWork(ctx)
}
// 通过 select 检查超时
func doWork(ctx context.Context) error {
ch := make(chan result, 1)
go func() {
// 耗时操作
r := heavyCompute()
ch <- r
}()
select {
case r := <-ch:
return r.err
case <-ctx.Done():
return ctx.Err() // context.DeadlineExceeded
}
}
HTTP 客户端超时
// 全局超时
client := &http.Client{
Timeout: 10 * time.Second, // 整个请求超时
}
// 更细粒度控制
client = &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // 连接超时
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second, // TLS 超时
ResponseHeaderTimeout: 10 * time.Second, // 等待响应头
IdleConnTimeout: 90 * time.Second, // 空闲连接
},
}
// 单请求超时(Context 控制)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
数据库超时
// GORM 查询超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var user User
err := db.WithContext(ctx).Where("id = ?", 1).First(&user).Error
if errors.Is(err, context.DeadlineExceeded) {
log.Println("查询超时")
}
gRPC 超时传递
// 客户端设置超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
// 服务端检查剩余时间
func (s *Server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
log.Printf("剩余超时时间: %v", remaining)
}
// 传递 Context 给下游
user, err := s.userService.Find(ctx, req.Id)
return user, err
}
超时链路传递
超时层级
下游超时必须小于上游超时。如果 Gateway 10s,Service A 应 < 10s,否则 Gateway 超时返回了,A 还在傻等。
// 中间件自动传递/缩短超时
func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
// 用 channel 追踪处理结果
done := make(chan struct{})
go func() {
c.Next()
close(done)
}()
select {
case <-done:
return
case <-ctx.Done():
c.JSON(504, gin.H{"error": "request timeout"})
c.Abort()
}
}
}
常见面试问题
Q1: context.WithTimeout 和 http.Client.Timeout 有什么区别?
答案:
http.Client.Timeout:包含连接、重定向、读取 Body 的整体超时context.WithTimeout:更灵活,可以在请求处理的任何环节检查- 建议两者都设:Client.Timeout 做兜底,Context 做精细控制
Q2: defer cancel() 为什么是必须的?
答案:即使 Context 已超时,cancel() 会释放关联的资源(timer、goroutine)。不调用会导致资源泄漏,直到父 Context 被取消。