← 中文资源

简体中文版本

Context 超时控制 HTTP 请求

为出站 HTTP 调用设置硬性截止时间:context 超时贯穿 DNS、连接、响应体读取全过程,超时错误显式区分返回。

阅读英文原文

示例说明

不带截止时间的出站请求会随下游卡死无限等待,拖垮整条调用链。WithTimeout 派生的 context 传进 NewRequestWithContext 后覆盖整个请求生命周期——DNS 解析、TCP 连接、TLS 握手、读响应体,任何阶段超时都会中断返回错误。errors.Is 判别 DeadlineExceeded 让超时与普通网络错误可分类处理。

技术要点

  • WithTimeout 派生带截止时间的上下文
  • defer cancel 释放计时器资源
  • DeadlineExceeded 与普通错误分型

代码解读

queryShipment 先派生三秒超时的 context,请求构造时注入;错误分支用 errors.Is 识别超时并附上耗时说明。响应体解码同样在 context 管辖内——缓慢的响应体传输也会被掐断。结构体的 json 标签完成字段映射。

使用建议

超时值按下游 P99 加倍的经验设定。context 也可由上游传入实现预算逐层递减。持续超时应触发熔断而非无限重试。

代码示例

package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"
)

type Shipment struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func queryShipment(ctx context.Context, baseURL, id string) (*Shipment, error) {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel() // 必须释放计时器资源

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/shipments/"+id, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			return nil, fmt.Errorf("shipment %s timed out after 3s: %w", id, err)
		}
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	var shipment Shipment
	if err := json.NewDecoder(resp.Body).Decode(&shipment); err != nil {
		return nil, fmt.Errorf("decode body: %w", err)
	}
	return &shipment, nil
}

func main() {
	s, err := queryShipment(context.Background(), "https://api.example.com", "sh-1")
	fmt.Println(s, err)
}