示例说明
不带截止时间的出站请求会随下游卡死无限等待,拖垮整条调用链。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)
}