Go-Zero 中如何调用外部接口(附示例)

在 go-zero 微服务里"调外部接口"通常指调用第三方 HTTP API(支付、行情、短信等)。框架内置的 rest/httpc 包能在"几乎零额外代码"的情况下,自动叠加超时、熔断、链路追踪等治理能力。本文给出当前版本(master/latest)真实可用的写法与完整示例。

httpc.Do 自动熔断 path/form/json 映射 svc 注入

1. 在 go-zero 里"调用外部接口"有几种方式

按"被调用方"与"治理强度"区分,推荐度依次递减。

场景推荐做法治理(超时/熔断/追踪)
调用第三方外部 HTTP APIrest/httpchttpc.NewService / Service.Do自动:按服务名熔断 + OTEL 追踪
调用内部其他 RPC 服务zrpc clientclient.NewXxx(zrpc.MustNewClient(...))自动:服务发现 + 超时 + 熔断
快速脚本 / 临时调用标准库 net/http无,需自己加(不推荐用于生产)
结论:生产环境调外部 HTTP,请用 httpc。它和 rest 服务端共用同一套中间件语义,调用失败会自动计入以"服务名"为维度的熔断器

2. httpc 实战(当前版本真实 API)

先说最重要的版本事实:当前 go-zero 的 httpc 没有 Get/Post 便捷方法,统一用 Service.Do(ctx, method, url, data)data 会按标签自动映射成路径/查询/JSON/Header。

2.1 最小可运行示例(调用一个外部行情接口)

package main import ( "context" "encoding/json" "fmt" "net/http" "time" "github.com/zeromicro/go-zero/rest/httpc" ) // 请求参数:form 标签 → 自动变成 URL 查询参数 type PriceReq struct { Symbol string `form:"symbol"` } // 响应体:按 JSON 反序列化 type PriceResp struct { Symbol string `json:"symbol"` Price float64 `json:"price"` } func main() { // name 用于熔断统计与链路追踪;timeout 通过底层 http.Client 设置 cli := httpc.NewServiceWithClient("price-api", &http.Client{Timeout: 3 * time.Second}) req := PriceReq{Symbol: "BTC"} resp, err := cli.Do(context.Background(), http.MethodGet, "https://api.example.com/v1/price", req) if err != nil { // 这里会收到:熔断触发 / 超时 / DNS / 连接拒绝 等错误 panic(err) } defer resp.Body.Close() // httpc 不会因 4xx/5xx 自动报错,必须自己判状态码 if resp.StatusCode != http.StatusOK { panic(fmt.Sprintf("unexpected status: %d", resp.StatusCode)) } var out PriceResp if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { panic(err) } fmt.Printf("price of %s = %v\n", out.Symbol, out.Price) }
两个易错点:
① 当前版本只有 Do(ctx, method, url, data)没有 client.Get(url, &v) 这种写法(老版本才有)。
② 必须手动 resp.Body.Close() 且手动判断 resp.StatusCode,否则 5xx 会被熔断计数而你的代码却以为成功了。

2.2 POST 发送 JSON 体

type CreateOrderReq struct { UserID int64 `json:"user_id"` // json 标签 → 请求体 Amount int64 `json:"amount"` } type CreateOrderResp struct { OrderID string `json:"order_id"` } resp, err := cli.Do(ctx, http.MethodPost, "https://api.example.com/v1/orders", CreateOrderReq{UserID: 1001, Amount: 99})

json 标签的结构体被编码进请求体,并自动设置 Content-Type: application/json

3. 请求参数的自动映射(data 标签规则)

Dodata 通过 go-zero 的 mapping 包解析,按结构体 tag 决定放哪里。这是 httpc 比裸 net/http 好用的地方。

tag含义放到请求的哪里示例
path路径变量替换 URL 中的 :nameid int64 `path:"id"`/users/123
form表单/查询URL 查询字符串(GET 常用)symbol string `form:"symbol"`
jsonJSON 体请求体(POST/PUT)amount int64 `json:"amount"`
header请求头HTTP Headertoken string `header:"X-Token"`
// 一个请求同时用 path + query + header type GetUserReq struct { ID int64 `path:"id"` // → /users/123 Fields string `form:"fields"` // → ?fields=name,age Trace string `header:"X-Trace"` // → Header } resp, err := cli.Do(ctx, http.MethodGet, "https://api.example.com/users/:id", req)

4. 在 go-zero 服务内部集成(最常用形态)

真实项目里,外部客户端应创建一次并注入 ServiceContext,在 logic 里调用,避免每次请求都新建连接。

4.1 在 ServiceContext 注入

// internal/svc/servicecontext.go type ServiceContext struct { Config config.Config UserAPI httpc.Service // 外部 HTTP 服务 UserRPC userclient.User // 内部 RPC(见第 6 节) } func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, // 用 WithClient 设置超时;同一个 name 共享一个熔断器 UserAPI: httpc.NewServiceWithClient("user-api", &http.Client{Timeout: 2 * time.Second}), UserRPC: userclient.NewUser(zrpc.MustNewClient(c.UserRPC)), } }

4.2 在 logic 里调用(注意用请求自带的 ctx)

// internal/logic/getprofilelogic.go func (l *GetProfileLogic) GetProfile(req *types.Req) (*types.Resp, error) { r := UserAPIReq{ID: req.ID} resp, err := l.svcCtx.UserAPI.Do(l.ctx, http.MethodGet, "https://user-svc.internal/profile", r) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("user-api %d", resp.StatusCode) } var out UserAPIResp if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, err } return &types.Resp{Name: out.Name}, nil }
为什么传 l.ctx这样外部调用的链路追踪(OTEL span)能自动串联到当前请求,超时也会随父 context 一起生效——这是 httpc 相比裸客户端的隐藏价值。

5. 自定义 Header / 超时 / 鉴权

httpc 的 Option 就是 func(r *http.Request) *http.Request,用它统一加签名、Token、Trace。

5.1 通过 Option 加 Header

authOpt := func(r *http.Request) *http.Request { r.Header.Set("Authorization", "Bearer "+token) return r } // 第二个参数开始都是 Option cli := httpc.NewService("payment-api", authOpt) // 也可直接传 *http.Request(更灵活) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req.Header.Set("X-Env", "prod") resp, err := cli.DoRequest(req)

5.2 超时与重试

// 超时:交给底层 http.Client cli := httpc.NewServiceWithClient( "search-api", &http.Client{Timeout: 800 * time.Millisecond}) // 重试:httpc 自身不带重试 Option, // 用循环/breaker 语义自己包一层: for i := 0; i < 3; i++ { resp, err = cli.Do(ctx, m, url, data) if err == nil { break } }
超时别用默认客户端:NewService 默认用 http.DefaultClient无超时),生产务必用 NewServiceWithClient 指定 Timeout,否则下游卡死会拖垮你的服务。

6. 顺带:调用内部 RPC 服务(zrpc)

如果"外部接口"指的是集群里另一个你自己的服务,优先用 gRPC(zrpc),比 HTTP 更高效且自带服务发现。

// svc 里注入(配置写 etcd 地址,自动发现) UserRPC: userclient.NewUser(zrpc.MustNewClient(c.UserRPC)) // logic 里直接调用,像调本地函数 u, err := l.svcCtx.UserRPC.GetUser(l.ctx, &user.GetUserReq{Id: req.ID}) if err != nil { return nil, err }

配置(etc/xxx.yaml)示例:

UserRPC: Etcd: Hosts: - "127.0.0.1:2379" Key: "user.rpc"

7. 熔断行为与常见坑

httpc 自动按"服务名"接入熔断器。理解它怎么判定成功/失败,能少踩很多坑。

✅ 计为"成功/可接受"

HTTP 状态码 < 500(含 4xx)、context 被取消(用户主动取消)。

❌ 计为"失败"(累积触发熔断)

状态码 ≥ 500、超时(DeadlineExceeded)、网络错误(连接拒绝 / DNS 失败)。

你的 logic httpc.Service.Do 外部 HTTP API 熔断器(按 name) ≥500/超时/网络错→计数 响应
图:每次 Do 都会经过以服务名为维度的熔断器;异常响应累积到阈值后自动"断开",快速失败保护自身。

常见坑清单

  • 忘了判状态码:4xx/5xx 不会让 Do 返回 err,一定要 resp.StatusCode 判断。
  • 忘了 Close Body:不关会导致连接泄漏、文件描述符耗尽。
  • 每次请求 New 一个 client:应在 svc 注入、复用;否则失去连接复用与统一熔断。
  • 用默认客户端无超时:务必 NewServiceWithClient 设 Timeout。
  • 服务名随意起:同名服务共享一个熔断器,建议用稳定、有业务含义的名字(如 user-api)。

8. 完整可运行示例(独立 main)

把上面的点串起来:带超时、判状态码、读 JSON、处理错误。

package main import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "time" "github.com/zeromicro/go-zero/rest/httpc" ) type QuoteReq struct { Symbol string `form:"symbol"` } type QuoteResp struct { Symbol string `json:"symbol"` Price float64 `json:"price"` } // 封装成一个带超时+熔断的小客户端 type MarketClient struct { svc httpc.Service } func NewMarketClient() *MarketClient { return &MarketClient{ svc: httpc.NewServiceWithClient("market-api", &http.Client{Timeout: 2 * time.Second}), } } func (c *MarketClient) GetPrice(ctx context.Context, symbol string) (*QuoteResp, error) { resp, err := c.svc.Do(ctx, http.MethodGet, "https://api.example.com/v1/quote", QuoteReq{Symbol: symbol}) if err != nil { return nil, fmt.Errorf("call market-api: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("market-api status %d: %s", resp.StatusCode, body) } var out QuoteResp if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return nil, errors.Join(fmt.Errorf("decode market-api"), err) } return &out, nil } func main() { c := NewMarketClient() q, err := c.GetPrice(context.Background(), "ETH") if err != nil { panic(err) } fmt.Printf("%s = %.2f\n", q.Symbol, q.Price) }
小结:go-zero 调外部接口 = httpc.NewService(WithClient) 创建一次 → Service.Do(ctx, method, url, data) 调用 → 手动判状态码 + 关 Body + 解码。它自动给你叠加按服务名的熔断OTEL 链路追踪,比裸 net/http 更适合生产。