← 中文资源

简体中文版本

Go HTTP 请求超时与上下文

为单次请求设置 context 截止时间,区分超时与其他传输错误,并限制响应体读取大小。

阅读英文原文

功能说明

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")
	}
}