用 http.Client 直接发请求最稳妥,需自定义并复用 *http.Client,显式设 Timeout 和 Transport,注入 service,URL 等配置化,关键依赖可选 resty 或 resilience-go 封装,日志 trace 必须透传。

用 http.Client 直接发请求是最稳妥的方式
Kratos 本身不封装外部 HTTP 调用逻辑,它只管“被调用”(对外暴露 API),不负责“去调用别人”。所以你得自己选一个可靠的 HTTP 客户端 —— 标准库的 http.Client 就够用,不需要引入 Gin、Resty 或其他第三方 HTTP 库来增加复杂度。
常见错误现象:
– 在 service 层直接用 http.Get() 忘记设置超时 → 请求卡住,拖垮整个服务
– 把 http.Client 声明成全局变量但没复用连接池 → 高并发下 fd 耗尽、TIME_WAIT 爆满
– 在 handler 里 new 一个 http.Client 每次都用 → 连接无法复用,性能断崖式下跌
- 始终用自定义的
*http.Client,显式设置Timeout、Transport -
Transport必须复用,推荐在internal/data或internal/pkg里初始化一次,注入到 service - 不要用
context.Background()作为请求上下文;用 handler 传进来的ctx,让它能随 API 请求取消而自动中断下游调用
示例片段:
var externalClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}
<p>func (s *MyService) CallThirdParty(ctx context.Context, id string) (string, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", "<a href="https://www.php.cn/link/4f52e1c157cc9f688a94a859bb4c9ec1">https://api.example.com/v1/item/"+id</a>, nil)
resp, err := externalClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// ... 处理 body
}
</p>
别在 internal/service 里硬编码 URL 和参数
URL、超时、重试策略这些属于配置项,不是业务逻辑。硬编码会导致:测试难 mock、多环境(dev/staging/prod)切换靠改代码、上线后想调参得发版。
- 把外部服务地址、基础路径、默认超时写进
configs/config.yaml,例如:third_party.api.base_url: "https://api.example.com" - 用 Kratos 的
conf.Conf+conf.Load加载,再通过依赖注入传给 service - 如果该外部接口有认证(如 Bearer Token),Token 获取逻辑建议单独抽成
internal/data/auth.go,不要塞进 service 方法里
需要重试或熔断?用 go-resty 或 resilience-go 封装一层
标准 http.Client 不带重试、熔断、降级能力。如果你调用的是关键外部依赖(比如支付网关、短信平台),裸用 http.Client 风险太高。
此时可引入轻量封装:
-
go-resty/resty/v2:适合需要简单重试 + JSON 自动编解码的场景,API 简洁,无侵入性 -
resilience-go:更底层,支持 circuit breaker、rate limiter、timeout wrapper,适合对稳定性要求极高的链路 - ⚠️ 注意:不要在
data层同时引入 resty 和 resilience-go,选一个并统一用到底;混用会增加调试成本
示例(resty):
client := resty.New().SetBaseURL(conf.ThirdParty.API.BaseURL)
client.SetRetryCount(2).SetTimeout(3 * time.Second)
<p>resp, err := client.R().
SetContext(ctx).
SetResult(&Resp{}).
Get("/v1/order/" + orderID)
</p>
日志和 trace 必须透传,否则排查等于盲人摸象
调用外部接口失败时,如果日志里只有“failed to call xxx”,没有原始请求 URL、状态码、耗时、trace ID,基本没法定位是网络问题、对方挂了、还是你拼错了 path。
- 所有外部调用前后,打结构化日志:包含 method、url、status、duration、error(如有)
- 用
kratos/pkg/transport/http提供的tracing.TraceIDFromContext(ctx)拿 trace ID,加到日志字段里 - 如果用了 OpenTelemetry,确保
http.Client的 transport 包了一层otelhttp.NewTransport,让 outbound 请求也带上 span
容易被忽略的一点:外部接口返回 4xx/5xx 并不总是错误,比如 404 可能表示资源不存在,业务上要区分处理;但日志里不能只写 “status=404”,得附带 response body 片段(脱敏后)或 error code 字段。











