网络请求

Go 标准库已经提供完整的 HTTP 客户端。第三方库主要用于简化 JSON、认证、重试、调试和中间件等常见操作。

名称特点适用场景
net/http标准库、生态兼容性最好、控制粒度高大多数项目和公共 SDK
Resty链式 API、JSON 编解码、认证、中间件和重试常规 REST API 调用
ReqAPI 简洁,内置调试、自动解码、重试和 HTTP/3偏好开箱即用体验的新项目
go-retryablehttpnet/http 之上提供退避重试需要保留标准库接口并增强重试
fasthttp独立 HTTP 实现,针对极高吞吐和低分配优化性能测试证明 net/http 是瓶颈的特殊场景

通常从 net/http 开始;想减少业务 API 的样板代码时选择 Resty 或 Req;只缺少可靠重试时选择 go-retryablehttp。fasthttp 与 net/http 生态不完全兼容,不应仅因为名称里有“fast”就默认使用。

net/http

自定义并复用 http.Client,不要在每次请求时创建新客户端:

client := &http.Client{
    Timeout: 10 * time.Second,
}

request, err := http.NewRequestWithContext(ctx, http.MethodGet,
    "https://api.example.com/users/1", nil)
if err != nil {
    return err
}
request.Header.Set("Accept", "application/json")

response, err := client.Do(request)
if err != nil {
    return err
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
    return fmt.Errorf("unexpected status: %s", response.Status)
}

var user User
if err := json.NewDecoder(response.Body).Decode(&user); err != nil {
    return err
}

标准库代码稍长,但没有额外依赖,并且容易通过自定义 http.RoundTripper 添加日志、追踪、认证或 Mock。

Resty

Resty 适合频繁调用 JSON API 的业务项目。当前稳定版使用 v2 导入路径:

go get github.com/go-resty/resty/v2
client := resty.New().
    SetBaseURL("https://api.example.com").
    SetTimeout(10 * time.Second).
    SetRetryCount(3)

var user User
response, err := client.R().
    SetContext(ctx).
    SetHeader("Accept", "application/json").
    SetAuthToken(token).
    SetResult(&user).
    Get("/users/1")
if err != nil {
    return err
}
if response.IsError() {
    return fmt.Errorf("unexpected status: %s", response.Status())
}

可以在 Client 级别统一配置 Base URL、Header、认证和中间件,再在单次请求中覆盖。

Req

Req 提供类似 Resty 的链式 API,并强化了开发调试、自动类型识别、请求重试和协议支持。

go get github.com/imroc/req/v3
client := req.C().
    SetBaseURL("https://api.example.com").
    SetTimeout(10 * time.Second).
    SetCommonBearerAuthToken(token)

var user User
response, err := client.R().
    SetContext(ctx).
    SetSuccessResult(&user).
    Get("/users/1")
if err != nil {
    return err
}
if !response.IsSuccessState() {
    return fmt.Errorf("unexpected status: %s", response.Status)
}

开发模式可能输出请求头和请求体,启用前应确认日志不会泄漏 Token、Cookie 或个人信息。

go-retryablehttp

只想在标准客户端上增加指数退避重试时,可以使用 go-retryablehttp:

go get github.com/hashicorp/go-retryablehttp
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 200 * time.Millisecond
client.RetryWaitMax = 2 * time.Second
client.HTTPClient.Timeout = 10 * time.Second

request, err := retryablehttp.NewRequestWithContext(
    ctx,
    http.MethodGet,
    "https://api.example.com/users/1",
    nil,
)
if err != nil {
    return err
}

response, err := client.Do(request)

StandardClient 可以返回普通的 *http.Client,便于接入只接受标准库客户端的 SDK。

重试边界

网络失败不代表服务端没有完成操作。重试前需要判断请求是否幂等:

  • GETHEAD 等读取请求通常可以重试。
  • PUTDELETE 是否可重试取决于服务端实现。
  • POST 默认不应自动重试;支付、下单等操作应使用幂等键。
  • 重试次数、单次超时和总 Deadline 应同时受控,并加入指数退避与随机抖动。
  • 429503 响应应在可信范围内遵守 Retry-After

实践建议

  • 复用 Client 和底层连接池,不要为每个请求创建 Client。
  • 每个请求都传递 context.Context,设置合理的超时和取消条件。
  • 使用 net/http 时及时关闭响应体;需要复用连接时还应完整读取响应体。
  • 限制错误响应体的读取大小,避免把超大响应全部载入内存。
  • 日志应记录方法、主机、状态码、耗时和请求 ID,但不能记录认证信息和敏感 Body。
  • 使用 httptest.Server 或自定义 Transport 测试客户端,不要让单元测试依赖真实外部服务。