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 ./...
生成目录包含 config、handler、logic、svc、types 和路由。生成器及模板锁定版本;不要直接改生成 handler。复杂业务让 logic 调用独立 usecase,以供多入口复用。
3. REST 请求生命周期与关键 API
rest.MustNewServer(conf) 在启动错误时终止;需要处理错误时用 rest.NewServer。AddRoute 注册路由,Use 添加 middleware,route option 设置 JWT、超时和 body 上限。handler 用 httpx.Parse 解析,以 httpx.OkJsonCtx 或 ErrorCtx 响应。
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.RestConf 或 zrpc.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 定义契约。字段编号不能复用,删除字段要 reserved。goctl 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 完整技术体系学习路线:从语法、并发到框架、中间件与 AI
- 上一篇:Go OpenAPI 与 Swagger:契约优先、代码生成和接口文档治理
- 下一篇:Go Kratos 实战:分层、HTTP/gRPC 双协议与可观测治理
- 延伸:Go gRPC 与 Protobuf 完整基础:IDL、Unary、Stream 与拦截器
- 延伸:Go 服务发现与配置中心:etcd、Consul、Nacos 的正确边界
- 延伸:Go 服务韧性设计:超时、重试、限流、熔断与隔离舱
- 延伸:Go OpenTelemetry 实战:Trace、Metric、Context 与 OTLP
官方资料
本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论