go-zero 里,怎么调「别人的服务」

内部服务(自己家的 RPC)和外部服务(第三方 HTTP 接口)调用方式不同。这篇讲清规范、配置、svc 注入,以及两端各自的示例代码。

总览 内部服务 zrpc 外部服务 httpc 配置与注入 内外对比 易踩坑

一句话区分:内部走 zrpc,外部走 httpc

go-zero 把「调用」也纳入了它的治理哲学:内部微服务用 gRPC(zrpc),外部 HTTP 接口用 httpc。两者都通过 svc.ServiceContext 注入,不在 logic 里现建连接。

你的 logic 用 svcCtx.* 调 内部服务(自家 RPC) user / order / pay 服务 协议:gRPC(zrpc) svc.ServiceContext UserRpc / PayHttp 依赖在 main 建一次 外部服务(第三方) 支付/短信/地图 API 协议:HTTP(httpc)

核心规范:无论内部外部,客户端都在 main → svc.ServiceContext 里建一次,logic 只管「调」,不负责「连」。 这样连接数可控、配置外置、测试可替换 —— 和「不用全局变量」那篇是一回事。

调自家 RPC 服务(推荐 zrpc / gRPC)

如果你调用的是另一个 go-zero 服务,双方用 proto 定义契约,goctl 生成客户端,注入 svc 后直接调。

① 定义 proto(被调用方 user 服务)

// user.proto
syntax = "proto3";
package user;

service User {
  rpc GetUser (GetUserReq) returns (GetUserResp);
}

message GetUserReq { int64 id = 1; }
message GetUserResp {
  int64  id    = 1;
  string name  = 2;
}

② 生成 RPC 代码(含客户端)

goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. \
  --style gozero     # 生成 user.pb.go + 服务端 + 客户端

③ 在 svc 里注入 RPC 客户端

// internal/svc/servicecontext.go
import user "yourorg/app/user/rpc/user"  // 生成的客户端包

type ServiceContext struct {
    Config  config.Config
    UserRpc user.User            // ✅ RPC 客户端,注入进来
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:  c,
        UserRpc: user.NewUser(c.UserRpc), // 连接只建一次
    }
}

④ 在 logic 里调用(业务侧)

// internal/logic/ordercreate.go
func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateReq) (*types.OrderCreateResp, error) {
    // 直接通过 svcCtx 调内部 user 服务,无需自己建连接
    u, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.GetUserReq{Id: req.UserId})
    if err != nil {
        return nil, err
    }
    // ...用 u.Name 继续写下单逻辑
    return &types.OrderCreateResp{Ok: true}, nil
}
📌 内部调用之所以用 gRPC:性能高、强类型契约、自带超时/重试/熔断/服务发现(etcd)。go-zero 的 zrpc 把这些开箱即用,省去你手搓 HTTP 客户端。

调第三方 HTTP 接口(推荐 httpc)

调外部系统(支付、短信、地图、天气等)走 HTTP。go-zero 自带 rest/httpc:类型化请求、内置超时、熔断、还能走服务发现。

① 在 svc 里建 httpc 客户端(同样注入)

// internal/svc/servicecontext.go
import "github.com/zeromicro/go-zero/rest/httpc"

type ServiceContext struct {
    Config  config.Config
    PayHttp httpc.Service          // ✅ 外部支付网关客户端
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:  c,
        PayHttp: httpc.NewService(c.PayHttp), // 按配置建客户端
    }
}

② 在 logic 里发起调用

// internal/logic/pay.go
var payResp PayQueryResp
err := l.svcCtx.PayHttp.Do(l.ctx, http.MethodPost,
    "/v1/pay/query", req, &payResp)   // 自动超时+熔断
if err != nil {
    return nil, err
}

最简形式:没有 Target 时直接 Do

// 只想简单发个请求、不配服务发现
var resp WeatherResp
err := httpc.Do(ctx, http.DefaultClient, http.MethodGet,
    "https://api.weather.com/now?city=bj", nil, &resp)
📌 为什么不直接用 net/http?可以用,但 httpc 帮你统一了超时、熔断(breaker)、重试和服务发现,和 go-zero 的服务治理理念一致,也更容易被 svc 统一注入管理。

配置都外置到 yaml,连接都在 main 建

内部 zrpc 和外部 httpc 的配置同样写在 etc/xxx.yaml,由 config.go 映射,main 启动时注入 svc。

config.go(结构)
type Config struct {
    rest.RestConf
    UserRpc zrpc.RpcClientConf  // 内部
    PayHttp httpc.ServiceConf   // 外部
}
etc/xxx.yaml(配置)
UserRpc:
  Etcd:
    Hosts: [127.0.0.1:2379]
    Key:   user.rpc
PayHttp:
  Target: "https://pay.example.com"
  Timeout: 3s

这套「配置外置 + main 建连接 + svc 注入 + logic 调用」的流程,正是 go-zero 替代全局变量、统一管理外部依赖的标准做法(详见「全局变量与 svc」篇)。

内部 vs 外部:一张表看明白

维度内部服务(zrpc)外部服务(httpc)
协议gRPC(protobuf)HTTP / JSON
契约来源双方共享 .proto对方文档 / OpenAPI
生成方式goctl rpc protoc 出客户端手写请求/响应 struct
配置位置zrpc.RpcClientConf(含 etcd 发现)httpc.ServiceConf(Target/超时)
svc 字段类型user.User(接口)httpc.Service(接口)
服务发现etcd 自动可配 Target(也支持 etcd)
治理(超时/熔断)内置内置(breaker/timeout)

三个最容易犯的错

❌ 不要在 logic 里现建客户端

每次请求 new 一个连接成本极高且无法复用。客户端必须在 svc 建一次,logic 复用。

❌ 不要把地址/密钥硬编码在代码里

一律走 etc/xxx.yaml + config.go,方便多环境切换和保密。

❌ 不要用全局变量存客户端

全局 client 难测试、难替换、难感知生命周期。交给 svc 注入,与「不用全局变量」规范一致。