Go 基础体系 · 第 50/113 篇。示例统一基于 Go 1.26.4;核心片段可能省略 package 与 import,完整程序可直接按文中结构运行。

Go go-zero 完整入门:API、RPC、goctl 与微服务治理

本文以 Go 1.26.4 和已验证的 go-zero v1.10.3 为基准。go-zero 提供 REST、zRPC、生成、限流、熔断和可观测集成。它减少组件拼装,却不会自动给出领域边界、事务、幂等或拆分粒度。

使用 go-zero 不要求立刻拆微服务。仅在部署或故障隔离确实独立时拆 API/RPC。

1. 组件地图与请求路径

rest.Server 承载路由和 middleware;zRPC 在 gRPC 上增加配置、发现、拦截器和治理;goctl 根据 .api.proto 生成协议适配代码。core 组件按需使用。

Client
  -> Ingress / TLS / coarse rate limit
  -> user-api: generated handler -> logic -> ServiceContext
  -> user-rpc: zRPC interceptor -> generated server -> logic/model
  -> database / cache / message broker

API 层负责 HTTP 语义和边界鉴权,RPC 层负责领域能力。ServiceContext 用于显式装配依赖,不应退化为全局对象仓库。

2. .api 契约与 goctl 生成

.api 文件声明 REST 类型、路由、handler 和服务属性。协议输入与领域模型分开,避免内部字段被返回。

syntax = "v1"

type CreateArticleReq {
    Title string `json:"title" validate:"required,max=120"`
}
type ArticleResp {
    Id string `json:"id"`
    Title string `json:"title"`
    Version int64 `json:"version"`
}

@server (
    prefix: /api/v1
    group: article
    timeout: 3s
)
service article-api {
    @handler createArticle
    post /articles (CreateArticleReq) returns (ArticleResp)
    @handler getArticle
    get /articles/:id returns (ArticleResp)
}
go install github.com/zeromicro/go-zero/tools/goctl@v1.10.3
goctl api format --dir .
goctl api go -api article.api -dir .
go test ./...

生成目录包含 confighandlerlogicsvctypes 和路由。生成器及模板锁定版本;不要直接改生成 handler。复杂业务让 logic 调用独立 usecase,以供多入口复用。

3. REST 请求生命周期与关键 API

rest.MustNewServer(conf) 在启动错误时终止;需要处理错误时用 rest.NewServerAddRoute 注册路由,Use 添加 middleware,route option 设置 JWT、超时和 body 上限。handler 用 httpx.Parse 解析,以 httpx.OkJsonCtxErrorCtx 响应。

func CreateArticleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.CreateArticleReq
        if err := httpx.Parse(r, &req); err != nil {
            httpx.ErrorCtx(r.Context(), w, err)
            return
        }
        result, err := logic.NewCreateArticleLogic(r.Context(), ctx).CreateArticle(&req)
        if err != nil {
            httpx.ErrorCtx(r.Context(), w, err)
            return
        }
        httpx.OkJsonCtx(r.Context(), w, result)
    }
}

请求经过 middleware、路由和解析后,handler 构造 logic,logic 使用请求 context 调用依赖。handler 返回后不能使用 ResponseWriter。通过测试固定 httpx.Parse 行为,并在代理和 RestConf.MaxBytes 同时限制 body。

4. 配置与 ServiceContext 装配

配置嵌入 rest.RestConfzrpc.RpcServerConf,再声明下游。conf.MustLoad 加载并验证;启动时一次性创建长生命周期客户端。

Name: article-api
Host: 0.0.0.0
Port: 8888
Timeout: 3000
MaxConns: 10000
MaxBytes: 1048576
Telemetry:
  Name: article-api
  Endpoint: otel-collector:4317
  Sampler: 0.1
Database:
  DSN: ${ARTICLE_DSN}
type ServiceContext struct {
    Config config.Config
    Articles ArticleService
}

func NewServiceContext(c config.Config) *ServiceContext {
    repo := model.NewArticleModel(openDB(c.Database.DSN))
    return &ServiceContext{Config: c, Articles: service.NewArticleService(repo)}
}

密钥来自环境或 secret manager。DSN、JWT secret 缺失应启动失败。动态配置只用于可热变更的开关或限额,其他改变需要原子替换和回滚。

5. Logic、Model 与事务边界

logic 持有 context 和 ServiceContext。业务不变量放 usecase/service,SQL 放 repository;跨 model 事务由 usecase 定义。缓存一致性、幂等和 outbox 不是框架开关。

func (l *CreateArticleLogic) CreateArticle(req *types.CreateArticleReq) (*types.ArticleResp, error) {
    title := strings.TrimSpace(req.Title)
    if title == "" || utf8.RuneCountInString(title) > 120 {
        return nil, domain.ErrInvalidTitle
    }
    article, err := l.svcCtx.Articles.Create(l.ctx, service.CreateArticle{Title: title})
    if err != nil {
        return nil, fmt.Errorf("create article: %w", err)
    }
    return &types.ArticleResp{Id: article.ID, Title: article.Title, Version: article.Version}, nil
}

不要把生成请求类型传到数据库层。缓存版本要与 DB 更新和失效一起设计;自动缓存无法解决事务一致性。

6. zRPC、Proto 与连接生命周期

zRPC 使用 Protobuf 定义契约。字段编号不能复用,删除字段要 reservedgoctl rpc protoc 调用固定版本工具生成骨架。

syntax = "proto3";
package article.v1;
option go_package = "example.com/article/rpc/articlev1";

service ArticleService { rpc GetArticle(GetArticleRequest) returns (Article); }
message GetArticleRequest { string id = 1; }
message Article { string id = 1; string title = 2; int64 version = 3; }
goctl rpc protoc article.proto \
  --go_out=. --go-grpc_out=. --zrpc_out=. --style=go_zero
buf breaking --against '.git#branch=main'

服务端用 zrpc.MustNewServer 注册,客户端 zrpc.MustNewClient 长期复用。deadline 从 API 传给 RPC 和数据库,并预留编码与返回时间。

7. 错误模型:HTTP 与 gRPC 分层映射

领域层定义可判定错误,RPC 映射为 gRPC code,API 再映射为 HTTP 与 JSON。包装使用 %w 保留 errors.Is

httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, any) {
    requestID := traceIDFromContext(ctx)
    switch {
    case errors.Is(err, domain.ErrInvalidTitle):
        return http.StatusBadRequest, Problem{Code: "INVALID_TITLE", RequestID: requestID}
    case errors.Is(err, domain.ErrNotFound):
        return http.StatusNotFound, Problem{Code: "NOT_FOUND", RequestID: requestID}
    case errors.Is(err, context.DeadlineExceeded):
        return http.StatusGatewayTimeout, Problem{Code: "TIMEOUT", RequestID: requestID}
    default:
        logx.WithContext(ctx).Error(err)
        return http.StatusInternalServerError, Problem{Code: "INTERNAL", RequestID: requestID}
    }
})

全局 error handler 在启动时设置一次。响应不暴露内部 error,日志保留 wrapped error 与 trace ID。gRPC Unavailable 是否重试仍取决于幂等性和预算。

8. 取消、超时、重试与熔断

RestConf.Timeout 控制 handler 预算,route 也可指定 timeout。下游使用 l.ctx 或更短 deadline。取消应传播到 SQL、RPC 与 HTTP client;可靠后台任务写入持久队列。

仅读操作或有幂等键的写操作可重试,并共享整体 deadline。breaker 保护下游,load shedding 保护本实例;调用方要区分过载与业务失败。禁止 ingress、API、RPC client 层层重试。

超时后数据库事务可能已经提交。创建接口用客户端提供的 idempotency key 或业务唯一键保存结果,再次请求返回同一结果。对于未知提交状态,不能简单告诉用户“失败后重试”。

9. 可运行的综合 REST 示例

下面程序直接使用 go-zero v1.10.3 REST API,包含 body 上限、context、并发 store、错误映射和优雅停止。

package main

import (
    "context"
    "errors"
    "flag"
    "net/http"
    "strings"
    "sync"
    "sync/atomic"
    "unicode/utf8"

    "github.com/zeromicro/go-zero/core/logx"
    "github.com/zeromicro/go-zero/rest"
    "github.com/zeromicro/go-zero/rest/httpx"
)

var errInvalidTitle = errors.New("invalid title")

type createRequest struct { Title string `json:"title"` }
type article struct { ID uint64 `json:"id"`; Title string `json:"title"` }
type problem struct { Code string `json:"code"` }

type store struct {
    next atomic.Uint64
    mu sync.RWMutex
    data map[uint64]article
}

func (s *store) create(ctx context.Context, title string) (article, error) {
    if err := ctx.Err(); err != nil { return article{}, err }
    title = strings.TrimSpace(title)
    if title == "" || utf8.RuneCountInString(title) > 120 { return article{}, errInvalidTitle }
    value := article{ID: s.next.Add(1), Title: title}
    s.mu.Lock()
    s.data[value.ID] = value
    s.mu.Unlock()
    return value, nil
}

func main() {
    port := flag.Int("port", 8888, "listen port")
    flag.Parse()
    articles := &store{data: make(map[uint64]article)}

    httpx.SetErrorHandlerCtx(func(_ context.Context, err error) (int, any) {
        if errors.Is(err, errInvalidTitle) {
            return http.StatusBadRequest, problem{Code: "INVALID_TITLE"}
        }
        if errors.Is(err, context.DeadlineExceeded) {
            return http.StatusGatewayTimeout, problem{Code: "TIMEOUT"}
        }
        logx.Error(err)
        return http.StatusInternalServerError, problem{Code: "INTERNAL"}
    })

    server := rest.MustNewServer(rest.RestConf{Host: "127.0.0.1", Port: *port, Timeout: 3000, MaxBytes: 1 << 20})
    defer server.Stop()
    server.AddRoute(rest.Route{Method: http.MethodPost, Path: "/articles", Handler: func(w http.ResponseWriter, r *http.Request) {
        var request createRequest
        if err := httpx.Parse(r, &request); err != nil { httpx.ErrorCtx(r.Context(), w, err); return }
        created, err := articles.create(r.Context(), request.Title)
        if err != nil { httpx.ErrorCtx(r.Context(), w, err); return }
        httpx.OkJsonCtx(r.Context(), w, created)
    }}, rest.WithMaxBytes(1<<20))
    server.Start()
}
go mod init example.com/gozero-demo
go get github.com/zeromicro/go-zero@v1.10.3
gofmt -w main.go
go run . -port 8888
curl -sS -H 'Content-Type: application/json' -d '{"title":"Go contracts"}' http://127.0.0.1:8888/articles

server.Start() 已接入优雅关闭;复杂进程要统一管理 REST、RPC、消费者和资源关闭顺序。示例适合 -race 验证,但不提供持久性。

10. 测试生成层与业务层

Logic 测试覆盖输入、领域错误、取消和 timeout;handler 用 httptest 验证状态、body 与 Content-Type。数据库用真实兼容实例做集成测试,RPC 用 bufconn 验证 code、metadata 与 deadline。

func TestStoreConcurrentCreate(t *testing.T) {
    s := &store{data: make(map[uint64]article)}
    var group sync.WaitGroup
    for i := 0; i < 100; i++ {
        group.Go(func() {
            if _, err := s.create(t.Context(), "title"); err != nil { t.Error(err) }
        })
    }
    group.Wait()
    s.mu.RLock()
    defer s.mu.RUnlock()
    if len(s.data) != 100 { t.Fatalf("got %d articles", len(s.data)) }
}

CI 固定 Go、goctl、protoc 和插件,运行生成差异、go test -race ./... 与契约检查。重点覆盖 logic、middleware、错误映射和配置边界。

11. 性能、限流与容量

生产压测包含 JSON、认证、RPC、数据库和真实响应,观察 P99、吞吐、分配、连接池等待和主动拒绝。治理参数按实例 CPU 与业务延迟校准。

限流至少区分入口总量、租户/用户配额和昂贵操作,key 必须有界,避免攻击者制造无限 limiter 状态。熔断指标按下游与方法拆分,但 label 不带用户 ID。缓存能降低读延迟,却会引入击穿、雪崩和一致性;热点 key 使用 singleflight 或受控预热,不能让 fallback 返回越权旧数据。

12. 安全与生产部署边界

TLS 通常在 ingress 终止,内部 RPC 是否 mTLS 由威胁模型决定。JWT 校验不仅验证签名,还要检查算法、issuer、audience、expiry 和密钥轮换;route 上启用 rest.WithJwt 仍不能替代资源级授权。CORS 只允许明确 origin,凭证模式不能使用通配。限制 body、header、并发、上传和日志字段,所有输入到 SQL、模板、文件路径前按目标语境处理。

服务发现故障时明确启动策略:没有地址是快速失败、使用短期缓存还是降级;不能无限等待。滚动发布先 readiness 摘流量,等待发现传播,再让在途 REST/RPC 在总 deadline 内完成,最后关闭消费者、数据库和日志。数据库迁移使用 expand-and-contract,保证新旧实例同时运行期间兼容。

监控请求量、错误、延迟、饱和、shedding、breaker、连接池和积压;日志不记录 token 与敏感 body。readiness 不应因可降级下游抖动造成全体实例重启。

go-zero 的合理生产边界是:用 goctl 固定协议适配,用 ServiceContext 显式装配依赖,用 context 和治理组件限制故障传播,把事务、幂等、授权和服务拆分留在业务设计中。 框架统一工程语言,但系统可靠性仍取决于这些边界是否被写进代码和测试。


系列导航与关联阅读

官方资料

本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。