功能说明
fetchWithDeadline 在调用方 context 之上再包一层 context.WithTimeout,并把派生 context 绑定到 http.NewRequestWithContext。请求成功时返回状态码和最多 1MiB 的响应体;超时或 transport 失败时返回带上下文的错误。示例用 httptest 启动本地服务,分别请求快速与慢速端点,展示 50ms 截止时间如何中断慢请求。
关键代码
每个请求都 defer cancel,及时释放定时器与父 context 的引用。读取响应体使用 io.LimitReader,避免服务端返回无限流时耗尽内存。错误判断先看 ctx.Err() 是否为 context.DeadlineExceeded,再用百分号包裹该哨兵错误,调用方仍可用 errors.Is 分类。客户端自身保留 5 秒总超时,作为 context 失效之外的兜底。
使用注意
不要在 http.Client 上设置过长的 Timeout 来代替逐请求截止时间,否则不同接口无法使用不同预算。收到响应后必须关闭 Body;即使只关心状态码,也要读掉或关闭连接,便于复用。超时并不保证服务端停止计算,服务端应同时监听 r.Context().Done()。该示例不处理重定向与重试,生产环境还需考虑幂等性和总重试预算。
验证方式
运行 main 后,快速请求输出 status=200 和 body=ok,慢速请求 status=0,且错误被 errors.Is(err, context.DeadlineExceeded) 判定为真。可将慢速端点延迟调到 20ms 或把截止时间调到 500ms,观察成功路径。也可用 httptest.NewServer 在测试中替换端点,断言状态码、响应体长度和错误链。
代码示例
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"time"
)
type fetchResult struct {
Status int
Body string
}
func fetchWithDeadline(ctx context.Context, client *http.Client, url string, timeout time.Duration) (fetchResult, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fetchResult{}, fmt.Errorf("build request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fetchResult{}, fmt.Errorf("fetch %s: deadline exceeded after %s: %w", url, timeout, context.DeadlineExceeded)
}
return fetchResult{}, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return fetchResult{}, fmt.Errorf("read body: %w", err)
}
return fetchResult{Status: resp.StatusCode, Body: string(body)}, nil
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
})
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(200 * time.Millisecond):
fmt.Fprint(w, "late")
case <-r.Context().Done():
}
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
ctx := context.Background()
result, err := fetchWithDeadline(ctx, client, server.URL+"/fast", 500*time.Millisecond)
fmt.Printf("fast: status=%d body=%q err=%v\n", result.Status, result.Body, err)
result, err = fetchWithDeadline(ctx, client, server.URL+"/slow", 50*time.Millisecond)
fmt.Printf("slow: status=%d body=%q err=%v\n", result.Status, result.Body, err)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("slow request classified as a deadline error")
}
}