Appearance
go-zero 项目实战与最佳实践
本篇是 go-zero 系列教程的收官篇。我们将把前面学到的所有知识点整合,通过一个完整的电商系统实战案例,讲解项目结构规范、服务拆分、API 网关、服务间通信、数据库设计、缓存策略等工程实践;介绍 goctl 模板自定义与团队协作;总结常见坑与解决方案、性能优化建议;最后给出从零到上线的完整流程。
一、项目结构规范
1. 单体 vs 微服务目录结构
单体应用(小项目)
适合接口数较少、团队规模小的场景:
myapp/
├── api/ # 所有 .api 文件
│ ├── user.api
│ ├── product.api
│ └── order.api
├── rpc/ # 所有 .proto 文件
│ └── user.proto
├── internal/
│ ├── config/
│ ├── handler/
│ ├── logic/
│ ├── svc/
│ ├── types/
│ ├── middleware/
│ └── model/ # 所有数据访问层
├── etc/
│ └── myapp-api.yaml
├── go.mod
└── myapp.go微服务(多服务)
适合业务复杂、多团队协作的场景:
ecommerce/ # monorepo 根目录
├── app/ # 业务服务
│ ├── user/ # 用户服务
│ │ ├── api/ # HTTP 服务
│ │ │ ├── etc/
│ │ │ ├── internal/
│ │ │ ├── user.api
│ │ │ └── user.go
│ │ └── rpc/ # RPC 服务
│ │ ├── etc/
│ │ ├── internal/
│ │ ├── pb/
│ │ ├── user.proto
│ │ └── user.go
│ ├── product/
│ │ ├── api/
│ │ └── rpc/
│ ├── order/
│ │ ├── api/
│ │ └── rpc/
│ └── payment/
│ ├── api/
│ └── rpc/
├── common/ # 公共代码
│ ├── ctxdata/ # 上下文数据
│ ├── errorx/ # 错误定义
│ ├── middleware/ # 公共中间件
│ └── utils/
├── deploy/ # 部署文件
│ ├── docker/
│ ├── k8s/
│ └── docker-compose.yml
├── docs/ # 文档
├── go.mod
└── Makefile2. monorepo vs 多仓库
| 维度 | monorepo | 多仓库 |
|---|---|---|
| 代码共享 | 简单(直接 import) | 需发布版本 |
| 权限管理 | 难(所有人可见) | 易(按仓库分配) |
| CI/CD | 复杂(需增量构建) | 简单(各自独立) |
| 跨服务重构 | 简单(一次提交) | 复杂(多仓库协调) |
| 适合规模 | 中小团队 | 大团队 |
go-zero 项目推荐 monorepo,原因:
- goctl 生成的 client 包需要被其他服务 import,monorepo 下用
replace指向本地路径,开发体验好 - 公共代码(errorx、middleware)共享方便
- 跨服务重构成本低
3. 公共代码组织
common/
├── ctxdata/
│ └── ctxdata.go # 从 ctx 提取用户ID等
├── errorx/
│ ├── baseerror.go # 基础错误
│ └── code.go # 错误码定义
├── middleware/
│ ├── auth.go # 通用鉴权
│ └── log.go # 通用日志
└── utils/
├── id.go # ID 生成
└── time.go # 时间工具示例:
go
// common/ctxdata/ctxdata.go
package ctxdata
import (
"context"
"encoding/json"
)
func GetUserIdFromCtx(ctx context.Context) int64 {
uid, _ := ctx.Value("userId").(json.Number).Int64()
return uid
}
func GetUsernameFromCtx(ctx context.Context) string {
name, _ := ctx.Value("username").(string)
return name
}go
// common/errorx/baseerror.go
package errorx
import "fmt"
type Code int
const (
OK Code = 0
InvalidParam Code = 10001
Unauthorized Code = 10002
NotFound Code = 10003
InternalError Code = 10004
RateLimited Code = 10005
)
type BizError struct {
Code Code
Msg string
}
func (e *BizError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Msg)
}
func New(code Code, msg string) *BizError {
return &BizError{Code: code, Msg: msg}
}二、完整电商系统实战
1. 服务拆分:用户、商品、订单、支付
按业务领域拆分服务(DDD 思想):
| 服务 | 职责 | 数据库 |
|---|---|---|
| user-api / user-rpc | 用户注册、登录、信息管理 | user_db |
| product-api / product-rpc | 商品 CRUD、库存管理 | product_db |
| order-api / order-rpc | 订单创建、查询、取消 | order_db |
| payment-api / payment-rpc | 支付、退款 | payment_db |
每个服务有独立的数据库,避免数据耦合。需要跨服务查询时,通过 RPC 调用。
2. API 网关
电商系统通常需要一个 API 网关,统一处理:
- 鉴权
- 限流
- 路由分发
- 响应聚合
go-zero 的 API 服务可以做网关:
api
// gateway.api
syntax = "v1"
type (
// 用户
LoginRequest { Username string `json:"username"`; Password string `json:"password"` }
LoginResponse { Token string `json:"token"` }
// 商品
GetProductRequest { Id int64 `path:"id"` }
GetProductResponse { Id int64 `json:"id"`; Name string `json:"name"`; Price float64 `json:"price"` }
// 订单
CreateOrderRequest { ProductId int64 `json:"productId"`; Quantity int `json:"quantity"` }
CreateOrderResponse { OrderId int64 `json:"orderId"` }
)
@server (
group: public
prefix: /api/v1
)
service gateway-api {
@handler LoginHandler
post /login (LoginRequest) returns (LoginResponse)
@handler GetProductHandler
get /product/:id (GetProductRequest) returns (GetProductResponse)
}
@server (
group: auth
prefix: /api/v1
jwt: Auth
)
service gateway-api {
@handler CreateOrderHandler
post /order (CreateOrderRequest) returns (CreateOrderResponse)
}网关的 ServiceContext 注入所有下游 RPC:
go
// gateway/internal/svc/servicecontext.go
package svc
import (
"github.com/zeromicro/go-zero/zrpc"
"gateway/internal/config"
"user-rpc/userclient"
"product-rpc/productclient"
"order-rpc/orderclient"
"payment-rpc/paymentclient"
)
type ServiceContext struct {
Config config.Config
UserRpc userclient.UserClient
ProductRpc productclient.ProductClient
OrderRpc orderclient.OrderClient
PaymentRpc paymentclient.PaymentClient
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUserClient(zrpc.MustNewClient(c.UserRpc)),
ProductRpc: productclient.NewProductClient(zrpc.MustNewClient(c.ProductRpc)),
OrderRpc: orderclient.NewOrderClient(zrpc.MustNewClient(c.OrderRpc)),
PaymentRpc: paymentclient.NewPaymentClient(zrpc.MustNewClient(c.PaymentRpc)),
}
}3. 服务间通信
订单服务创建订单时,需要:
- 调用 product-rpc 查商品、扣库存
- 调用 user-rpc 查用户信息
- 写入订单
- 调用 payment-rpc 发起支付
go
// order-rpc/internal/logic/createorderlogic.go
func (l *CreateOrderLogic) CreateOrder(in *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
// 1. 查商品
product, err := l.svcCtx.ProductRpc.GetProduct(l.ctx, &productpb.GetProductRequest{Id: in.ProductId})
if err != nil {
return nil, status.Error(codes.FailedPrecondition, "product not found")
}
// 2. 扣库存
_, err = l.svcCtx.ProductRpc.DeductStock(l.ctx, &productpb.DeductStockRequest{
ProductId: in.ProductId,
Quantity: in.Quantity,
})
if err != nil {
return nil, status.Error(codes.FailedPrecondition, "deduct stock failed")
}
// 3. 查用户
user, err := l.svcCtx.UserRpc.GetUser(l.ctx, &userpb.GetUserRequest{Id: in.UserId})
if err != nil {
// 库存已扣,需要回滚(或走补偿)
l.svcCtx.ProductRpc.AddStock(l.ctx, &productpb.DeductStockRequest{
ProductId: in.ProductId,
Quantity: in.Quantity,
})
return nil, status.Error(codes.FailedPrecondition, "user not found")
}
// 4. 创建订单(写 DB)
order := &model.Order{
UserId: in.UserId,
ProductId: in.ProductId,
Quantity: in.Quantity,
Amount: product.Price * float64(in.Quantity),
Status: "pending",
CreateTime: time.Now(),
}
result, err := l.svcCtx.OrderModel.Insert(l.ctx, order)
if err != nil {
// 回滚库存
l.svcCtx.ProductRpc.AddStock(l.ctx, &productpb.DeductStockRequest{
ProductId: in.ProductId,
Quantity: in.Quantity,
})
return nil, status.Error(codes.Internal, "create order failed")
}
orderId, _ := result.LastInsertId()
// 5. 发起支付
_, err = l.svcCtx.PaymentRpc.CreatePayment(l.ctx, &paymentpb.CreatePaymentRequest{
OrderId: orderId,
Amount: order.Amount,
UserId: in.UserId,
})
if err != nil {
// 支付创建失败,订单置为"待支付",用户稍后重试
l.svcCtx.OrderModel.UpdateStatus(l.ctx, orderId, "payment_failed")
}
return &pb.CreateOrderResponse{OrderId: orderId}, nil
}4. 数据库设计
每个服务独立数据库,避免跨库 JOIN:
sql
-- user_db
CREATE DATABASE user_db;
USE user_db;
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password VARCHAR(128) NOT NULL,
email VARCHAR(128),
phone VARCHAR(20),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- product_db
CREATE DATABASE product_db;
USE product_db;
CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE stock_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT NOT NULL,
change_qty INT NOT NULL,
order_id BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- order_db
CREATE DATABASE order_db;
USE order_db;
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_status (status)
);
-- payment_db
CREATE DATABASE payment_db;
USE payment_db;
CREATE TABLE payments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNIQUE NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
pay_type VARCHAR(32),
trade_no VARCHAR(64),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);5. 缓存策略
| 数据 | 缓存策略 | 过期时间 |
|---|---|---|
| 用户信息 | Cache-Aside(自动缓存) | 1 小时 |
| 商品详情 | Cache-Aside | 30 分钟 |
| 商品库存 | 不缓存(强一致性) | - |
| 订单列表 | 不缓存(实时性要求高) | - |
| 订单详情 | 短期缓存 | 5 分钟 |
| 热门商品列表 | 多级缓存 | 10 分钟 |
商品库存不缓存的原因:库存变更频繁,缓存容易不一致,直接操作 DB + 行锁更安全。
go
// product-rpc/internal/logic/deductstocklogic.go
func (l *DeductStockLogic) DeductStock(in *pb.DeductStockRequest) (*pb.DeductStockResponse, error) {
// 用行锁扣库存(避免超卖)
result, err := l.svcCtx.ProductModel.ExecCtx(l.ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
return conn.ExecCtx(ctx,
"UPDATE products SET stock=stock-? WHERE id=? AND stock>=?",
in.Quantity, in.ProductId, in.Quantity)
})
if err != nil {
return nil, err
}
affected, _ := result.RowsAffected()
if affected == 0 {
return nil, status.Error(codes.FailedPrecondition, "insufficient stock")
}
// 记录库存变更日志
l.svcCtx.StockLogModel.Insert(l.ctx, &model.StockLog{
ProductId: in.ProductId,
ChangeQty: -in.Quantity,
OrderId: in.OrderId,
})
return &pb.DeductStockResponse{Success: true}, nil
}三、代码生成最佳实践
1. goctl 模板自定义
goctl 内置模板可能不满足团队需求,可以自定义:
bash
# 初始化本地模板(复制到 ~/.goctl 模板目录)
goctl template init
# 查看模板文件
ls ~/.goctl/api/
# handler.tpl logic.tpl svc.tpl types.tpl ...编辑模板,例如 logic.tpl,统一加入错误码和日志:
text
package {{.packageName}}
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
{{.imports}}
)
type {{.logicName}} struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func New{{.logicName}}(ctx context.Context, svcCtx *svc.ServiceContext) *{{.logicName}} {
return &{{.logicName}}{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *{{.logicName}}) {{.function}}({{.request}}) ({{.response}}, error) {
// TODO: add business logic
logx.WithContext(l.ctx).Info("{{.function}} called")
return nil, errorx.New(errorx.OK, "not implemented")
}注意:上面的模板文件中
\{\{.packageName\}\}等是 Go 模板语法,在 goctl 模板文件中使用是正常的。本段在文档中已对非代码块区域的花括号做了转义展示。在 goctl 实际模板文件中应写为\{\{.packageName\}\}(即 Go 模板原生语法)。
使用自定义模板生成代码:
bash
goctl api go -api user.api -dir . --home ~/.goctl2. 团队协作的 .api 文件管理
文件组织
app/user/api/
├── desc/ # 拆分的 .api 文件
│ ├── user.api # 用户相关
│ ├── auth.api # 鉴权相关
│ └── admin.api # 管理后台
└── user.api # 主文件,import desc 中的文件主文件 user.api:
api
syntax = "v1"
import "desc/user.api"
import "desc/auth.api"
import "desc/admin.api"
info (
title: "用户服务"
version: "v1"
)版本管理
.api 文件纳入 Git,每次接口变更都走 PR Review:
- 新增接口:新建 handler/logic
- 修改接口:注意向后兼容
- 废弃接口:标记
@deprecated,给迁移时间
接口文档自动化
CI 流程中用 goctl api doc 生成文档,发布到 Wiki:
bash
# CI 脚本
goctl api doc -api user.api -dir docs/user
goctl api doc -api product.api -dir docs/product
# 推送到文档站点3. model 生成与缓存
bash
# 从 DB 生成 model(带缓存)
goctl model mysql datasource \
-url "root:123456@tcp(mysql:3306)/user_db" \
-table "users" \
-dir app/user/rpc/internal/model \
-cache true \
--style=goZero
# 批量生成(脚本)
TABLES="users user_logs user_tokens"
for t in $TABLES; do
goctl model mysql datasource \
-url "$DSN" -table "$t" \
-dir app/user/rpc/internal/model \
-cache true --style=goZero
done四、常见坑与解决方案
1. 跨服务事务
问题:订单服务创建订单时,需要扣库存(product-rpc)+ 创建订单(order-db)+ 发起支付(payment-rpc),任一失败需要回滚。
go-zero 不内置分布式事务,常用方案:
方案一:补偿模式(Saga)
go
func (l *CreateOrderLogic) CreateOrder(in *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
// Step 1: 扣库存
_, err := l.svcCtx.ProductRpc.DeductStock(l.ctx, &productpb.DeductStockRequest{...})
if err != nil {
return nil, err // 失败,无需补偿
}
// Step 2: 创建订单
orderId, err := l.createOrderInDB(in)
if err != nil {
// 补偿:回滚库存
l.svcCtx.ProductRpc.AddStock(l.ctx, &productpb.DeductStockRequest{...})
return nil, err
}
// Step 3: 发起支付(异步,失败不回滚订单)
go func() {
_, err := l.svcCtx.PaymentRpc.CreatePayment(context.Background(), &paymentpb.CreatePaymentRequest{
OrderId: orderId,
...
})
if err != nil {
// 标记订单为"待支付",用户稍后重试
l.svcCtx.OrderModel.UpdateStatus(context.Background(), orderId, "payment_pending")
}
}()
return &pb.CreateOrderResponse{OrderId: orderId}, nil
}方案二:本地消息表
每个服务维护一个「消息表」,业务操作和消息写入同一个本地事务,异步投递消息到下游:
sql
-- order_db 中的消息表
CREATE TABLE local_messages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
biz_type VARCHAR(64) NOT NULL, -- 消息类型(如"订单创建")
biz_id BIGINT NOT NULL, -- 业务 ID(如订单 ID)
payload TEXT, -- 消息内容(JSON)
status VARCHAR(32) DEFAULT 'pending', -- pending / sent / failed
retry_count INT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);业务事务中同时写订单和消息:
go
err := l.svcCtx.OrderModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 写订单
result, err := session.ExecCtx(ctx, "INSERT INTO orders(...) VALUES(...)", ...)
if err != nil {
return err
}
orderId, _ := result.LastInsertId()
// 写消息(同一事务)
payload, _ := json.Marshal(map[string]any{"orderId": orderId, "userId": in.UserId})
_, err = session.ExecCtx(ctx, "INSERT INTO local_messages(biz_type,biz_id,payload) VALUES(?,?,?)",
"order_created", orderId, string(payload))
return err
})后台任务扫描消息表,投递到下游:
go
func (s *MessageScheduler) Run() {
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
messages, _ := s.messageModel.FindPending(context.Background(), 100)
for _, msg := range messages {
err := s.dispatch(msg)
if err != nil {
s.messageModel.IncrRetry(context.Background(), msg.Id)
} else {
s.messageModel.MarkSent(context.Background(), msg.Id)
}
}
}
}2. 数据一致性
缓存与 DB 一致性
go-zero 默认「先写 DB 再删缓存」,对绝大多数场景足够。强一致性场景用「延迟双删」或「订阅 binlog」。
跨服务数据一致性
避免跨服务 JOIN,用冗余字段或视图聚合:
go
// 订单列表需要展示商品名,但商品在 product_db
// 方案一:订单表冗余商品名(更新商品时同步)
// 方案二:API 网关聚合(先查订单列表,再批量查商品)
func (l *ListOrderLogic) ListOrder(req *types.ListOrderRequest) (*types.ListOrderResponse, error) {
// 1. 查订单列表
orders, total, _ := l.svcCtx.OrderRpc.ListOrder(l.ctx, &orderpb.ListOrderRequest{
UserId: req.UserId,
Page: req.Page,
PageSize: req.PageSize,
})
// 2. 批量查商品(避免 N+1)
productIds := extractProductIds(orders)
products, _ := l.svcCtx.ProductRpc.BatchGetProducts(l.ctx, &productpb.BatchGetProductsRequest{
Ids: productIds,
})
productMap := toMap(products)
// 3. 组装响应
var list []types.OrderVO
for _, o := range orders {
list = append(list, types.OrderVO{
OrderId: o.Id,
ProductName: productMap[o.ProductId].Name,
Amount: o.Amount,
Status: o.Status,
})
}
return &types.ListOrderResponse{Total: total, List: list}, nil
}3. 服务版本管理
API 版本
通过 URL 路径区分版本:
api
@server (
prefix: /api/v1
)
service user-api {
@handler GetUserV1Handler
get /user/:id returns (GetUserResponse)
}
@server (
prefix: /api/v2
)
service user-api {
@handler GetUserV2Handler
get /user/:id returns (GetUserV2Response) // 返回结构有变化
}RPC 版本
gRPC 不直接支持版本,常用做法:
- 包名加版本:
package user.v1、package user.v2 - 不同 proto 文件生成不同 client
- 灰度迁移:新版本服务同时部署,逐步切换流量
4. 常见坑
坑 1:context 传递丢失
错误:RPC 调用时不传 ctx
go
// ❌ 错误
resp, _ := l.svcCtx.UserRpc.GetUser(context.Background(), req)正确:传 l.ctx,保证 TraceID 传播和超时控制
go
// ✅ 正确
resp, err := l.svcCtx.UserRpc.GetUser(l.ctx, req)坑 2:goroutine 泄漏
错误:在 Logic 中启动 goroutine 但不管理生命周期
go
// ❌ 危险
go func() {
// 这个 goroutine 可能比请求活得久,ctx 已取消
doSomething(l.ctx)
}()正确:用独立 context 或 wait group
go
// ✅ 安全
go func() {
ctx := context.Background() // 不用请求 ctx
doSomething(ctx)
}()坑 3:缓存 key 冲突
不同实体用不同前缀,避免 key 冲突:
go
userIdKey := fmt.Sprintf("user:id:%d", id) // 用户
productIdKey := fmt.Sprintf("product:id:%d", id) // 商品坑 4:goctl 重新生成覆盖自定义代码
_gen.go 文件会被覆盖,自定义代码放在不带 _gen 的文件:
model/
├── usermodel.go # 接口 + NewUserModel(会被覆盖)
├── usermodel_gen.go # 自动生成的方法(会被覆盖)
├── usersmodel.go # 自定义方法(不会被覆盖,手动创建)
└── vars.go # 变量(会被覆盖)五、性能优化建议
1. 减少序列化开销
- protobuf 比 JSON 快 5-10 倍,服务间通信用 RPC(gRPC)
- 缓存序列化用 msgpack 或 protobuf,比 JSON 快
- 避免在热路径中频繁
json.Marshal/Unmarshal
2. 批量操作
go
// ❌ N+1 查询
for _, id := range ids {
user, _ := l.svcCtx.UserRpc.GetUser(l.ctx, &userpb.GetUserRequest{Id: id})
}
// ✅ 批量查询
users, _ := l.svcCtx.UserRpc.BatchGetUsers(l.ctx, &userpb.BatchGetUsersRequest{Ids: ids})3. 连接池复用
- MySQL 连接池:
SetMaxOpenConns(100)、SetMaxIdleConns(10) - Redis 连接池:go-zero 默认管理
- RPC 连接:zrpc.Client 复用,不要每次 new
4. 缓存优化
- 热点数据用本地缓存(L1)
- 大对象用 protobuf 序列化
- 控制缓存 value 大小(< 10KB)
5. goroutine 池
避免无限制创建 goroutine:
go
import "github.com/zeromicro/go-zero/core/syncx"
pool := syncx.NewTaskRunner(100) // 100 个 worker
func (l *SomeLogic) Process(items []Item) {
for _, item := range items {
pool.Scheduler(func() {
processItem(item)
})
}
}6. 数据库优化
- 索引:高频查询字段加索引
- 分页:用
WHERE id > ? LIMIT ?替代OFFSET - 读写分离:主库写,从库读
- 分库分表:单表数据超过 1000 万考虑
六、从零到上线的完整流程
1. 本地开发
bash
# 1. 启动依赖
docker-compose up -d mysql redis etcd jaeger
# 2. 创建数据库
mysql -h127.0.0.1 -uroot -p123456 < sql/init.sql
# 3. 生成代码
goctl api go -api user.api -dir app/user/api --style=goZero
goctl rpc protoc user.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=app/user/rpc --style=goZero
goctl model mysql datasource -url "$DSN" -table users -dir app/user/rpc/internal/model -cache --style=goZero
# 4. 编写业务逻辑
# 编辑 logic 文件
# 5. 本地运行
go run app/user/rpc/user.go -f app/user/rpc/etc/user.yaml
go run app/user/api/user.go -f app/user/api/etc/user-api.yaml2. CI/CD
.github/workflows/deploy.yml:
text
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Install goctl
run: go install github.com/zeromicro/go-zero/tools/goctl@latest
- name: Lint
run: go vet ./...
- name: Test
run: go test ./... -v -cover
- name: Generate code
run: |
find . -name "*.api" -exec goctl api go -api {} -dir $(dirname {}) --style=goZero \;
find . -name "*.proto" -exec goctl rpc protoc {} --zrpc_out=$(dirname {}) --style=goZero \;
- name: Build
run: |
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o bin/user-api app/user/api/user.go
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o bin/user-rpc app/user/rpc/user.go
- name: Build Docker image
run: |
docker build -t registry.cn-hangzhou.aliyuncs.com/myrepo/user-api:${{ github.sha }} -f app/user/api/Dockerfile .
docker build -t registry.cn-hangzhou.aliyuncs.com/myrepo/user-rpc:${{ github.sha }} -f app/user/rpc/Dockerfile .
- name: Push to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.cn-hangzhou.aliyuncs.com -u ${{ secrets.REGISTRY_USER }} --password-stdin
docker push registry.cn-hangzhou.aliyuncs.com/myrepo/user-api:${{ github.sha }}
docker push registry.cn-hangzhou.aliyuncs.com/myrepo/user-rpc:${{ github.sha }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to K8s
uses: steebchen/kubectl@v2.0.0
with:
config: ${{ secrets.KUBE_CONFIG }}
command: set image deployment/user-api user-api=registry.cn-hangzhou.aliyuncs.com/myrepo/user-api:${{ github.sha }} -n production3. K8s 部署
bash
# 1. 创建命名空间
kubectl create namespace production
# 2. 部署基础设施(MySQL/Redis/etcd 可用云服务或自建)
kubectl apply -f deploy/k8s/infrastructure/
# 3. 部署业务服务
kubectl apply -f deploy/k8s/user-rpc/
kubectl apply -f deploy/k8s/user-api/
kubectl apply -f deploy/k8s/product-rpc/
kubectl apply -f deploy/k8s/product-api/
kubectl apply -f deploy/k8s/order-rpc/
kubectl apply -f deploy/k8s/order-api/
# 4. 部署网关
kubectl apply -f deploy/k8s/gateway/
# 5. 验证
kubectl get pods -n production
kubectl get svc -n production4. 监控告警
yaml
# deploy/k8s/prometheus-alerts.yml
groups:
- name: ecommerce-alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_total{namespace="production",code=~"5.."}[5m]))
/ sum(rate(http_server_requests_total{namespace="production"}[5m])) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "生产环境错误率 > 5%"
- alert: RPCDown
expr: up{job=~".*-rpc"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "RPC 服务下线"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_duration_ms_bucket{namespace="production"}[5m])) by (le)
) > 2000
for: 5m
labels:
severity: warning
annotations:
summary: "P99 延迟 > 2s"告警推送到钉钉/企业微信:
yaml
# alertmanager.yml
route:
receiver: dingtalk
receivers:
- name: dingtalk
webhook_configs:
- url: "https://oapi.dingtalk.com/robot/send?access_token=xxx"
send_resolved: true七、最佳实践总结
1. 项目组织
- monorepo + 按业务领域拆分服务
- 公共代码集中到
common/包 .api/.proto/ model 文件版本化管理
2. 开发流程
- 先定义
.api/.proto契约 - 用 goctl 生成骨架
- 在 logic 层填业务
- 单元测试覆盖核心逻辑
3. 服务治理
- 限流:API 层 + 关键接口
- 熔断:go-zero 内置自适应
- 负载均衡:默认 p2c
- 服务发现:etcd
4. 数据层
- sqlx + goctl model + CachedConn
- 缓存策略:Cache-Aside + 单飞 + 空值缓存
- 跨服务事务:补偿模式 / 本地消息表
- 不缓存强一致性数据(如库存)
5. 可观测性
- 日志:logx + TraceID
- 监控:Prometheus + Grafana
- 链路追踪:OpenTelemetry + Jaeger
- 三者通过 TraceID 关联
6. 部署
- Docker 多阶段构建
- K8s + ConfigMap + Secret
- HPA 自动扩缩容
- CI/CD 自动化
八、系列教程总结
至此,go-zero 系列教程全部完成。回顾整个系列:
| 章节 | 核心内容 |
|---|---|
| 01 简介与快速开始 | go-zero 定位、goctl 安装、第一个 API/RPC |
| 02 API 服务开发 | .api 语法、goctl api、分层编写、CRUD 示例 |
| 03 RPC 服务开发 | .proto、goctl rpc、服务端/客户端、API 调用 RPC |
| 04 中间件与拦截器 | API 中间件、RPC 拦截器、JWT/CORS/限流实现 |
| 05 数据库与 Redis | sqlx、goctl model、redisz、CachedConn |
| 06 模型缓存 | CachedConn 原理、穿透/击穿/雪崩防护 |
| 07 服务治理 | 限流、熔断、负载均衡、服务发现 |
| 08 配置与部署 | YAML 配置、多环境、Docker、K8s |
| 09 可观测性 | logx、Prometheus、OpenTelemetry |
| 10 项目实战 | 电商系统、最佳实践、从零到上线 |
go-zero 的核心价值在于「工程化开箱即用」:通过 goctl 把样板代码自动化,通过框架内置把服务治理、可观测性标准化,让团队能聚焦业务逻辑。掌握这套体系,你就能高效地构建生产级的 Go 微服务系统。
九、小结
本篇通过一个完整的电商系统实战,整合了前面所有章节的知识点。要点回顾:
- 项目结构:monorepo + 按业务领域拆分服务,公共代码集中管理
- 服务拆分:用户、商品、订单、支付,每个服务独立数据库
- API 网关:统一鉴权、限流、路由分发、响应聚合
- 服务间通信:通过 RPC Client,注意 N+1 问题和批量查询
- 跨服务事务:补偿模式(Saga)或本地消息表
- goctl 模板自定义:统一团队代码风格
- 常见坑:context 传递、goroutine 泄漏、缓存 key 冲突、goctl 覆盖
- 性能优化:批量操作、连接池、本地缓存、goroutine 池
- 完整流程:本地开发 → CI/CD → K8s 部署 → 监控告警
希望这套教程能帮助你掌握 go-zero 微服务框架,构建出高质量的 Go 微服务系统。祝编程愉快!