最佳实践与避坑指南
本章汇总了 go-zero 社区经过大量生产实践验证的工程化最佳实践、常见陷阱和避坑经验。
工程化最佳实践
1. 约定优于配置
- 团队统一
.api规范、错误码规范、响应体结构 - 使用
goctl自动生成代替手写,避免人为失误 - 定制
goctl模板,一键生成符合团队标准的代码
2. ServiceContext 是唯一的依赖注入入口
// ✅ 正确:所有依赖在 ServiceContext 中初始化
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
UserRpc user.User
Redis *redis.Redis
KqPusher *kq.Pusher
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(sqlx.NewMysql(c.DataSource), c.Cache),
UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpcConf)),
Redis: redis.NewRedis(c.Redis.Host, ""),
KqPusher: kq.NewPusher(c.KafkaConf.Brokers, c.KafkaConf.Topic),
}
}
// ❌ 错误:在 logic 里临时创建连接
func (l *MyLogic) DoSomething() error {
conn := sqlx.NewMysql(dsn) // 不可以!
// ...
}3. 分层职责明确
Handler → 只做参数解析和响应序列化(不写业务逻辑)
Logic → 业务编排(调用 Model/RPC/缓存)
Model → 单表 CRUD + 缓存(goctl 生成)
Repository → 复杂查询、多表关联、聚合业务语义4. 微服务拆分原则
| 时机 | 触发条件 |
|---|---|
| 建议拆分 | 单一 service 目录超过 50 个文件 |
| 建议拆分 | 日活用户超过 100 万 |
| 建议拆分 | 接口 QPS 超过 1 万 |
| 不要过度拆分 | 一个接口一个服务 |
| 不要过早拆分 | 业务验证期就拆微服务 |
5. RPC 调用链设计
✅ 推荐:
API → User RPC → Order RPC → Payment RPC (单向)
❌ 禁止:
API → User RPC → Order RPC → User RPC (循环调用)// 串行改并行 — 使用 core/mr 包
import "github.com/zeromicro/go-zero/core/mr"
func (l *OrderLogic) GetOrderDetail(id int64) (*OrderDetail, error) {
var user *userRpc.GetUserResp
var product *productRpc.GetProductResp
err := mr.Finish(func() error {
user, err = l.svcCtx.UserRpc.GetUser(ctx, &userRpc.GetUserReq{Id: userId})
return err
}, func() error {
product, err = l.svcCtx.ProductRpc.GetProduct(ctx, &productRpc.GetProductReq{Id: productId})
return err
})
// ...
}6. 结构化日志是底线
// ✅ 正确:结构化日志 + trace_id
logx.WithContext(ctx).Infow("order created",
logx.Field("orderId", orderId),
logx.Field("userId", userId),
)
// ❌ 错误:普通日志,丢失 trace_id
log.Printf("order created: %d", orderId)7. 没有度量就没有优化
| 必看指标 | 工具 | 关注点 |
|---|---|---|
| QPS | Prometheus + Grafana | 流量趋势 |
| P95/P99 延迟 | Prometheus + Grafana | 慢请求 |
| 错误率 | Prometheus + Grafana | 异常趋势 |
| RPC 连接池 | go-zero 内置 | pool exhausted |
| 熔断器状态 | go-zero 内置 | 下游故障 |
| 消息堆积 | Kafka/asynq | 消费能力 |
常见陷阱全览
基础设施
| 陷阱 | 根因 | 解决 |
|---|---|---|
🚨 go mod init 短名称 |
import 链断裂 | 用完整路径 github.com/org/project |
| 🚨 etcd 未启动但配置了 | RPC 启动失败 | 先启动 etcd,或用直连模式 |
| 🚨 配置 YAML key 大小写 | conf.MustLoad 映射失败 |
Go struct 字段名 = YAML key 名 |
| 🚨 API 和 RPC 共用 Config struct | 字段不兼容 | 各自定义独立的 Config |
RPC 调用
| 陷阱 | 根因 | 解决 |
|---|---|---|
| 🚨 RPC client 首次调用 2-3 秒 | 懒加载建连 | main 函数中预热 client.Ping(ctx) |
| 🚨 每个 logic 里 new client | 反复建连 + 连接泄漏 | client 注入 ServiceContext,单例使用 |
| 🚨 RPC 超时未设置 | gRPC 默认无限等待 | 必须 context.WithTimeout |
| 🚨 MaxPoolSize=10 不够 | 突发扩容连接耗尽 → 静默丢弃 | 升级 v1.8.3+,监控连接池指标 |
| 🚨 循环 RPC 调用 | A→B→C→A | 用 core/mr 并行替代串行依赖 |
数据库与缓存
| 陷阱 | 根因 | 解决 |
|---|---|---|
| 🚨 认为 go-zero 自有事务 | 默认无事务 | 手动 TransactCtx |
| 🚨 事务内用全局 DB | 无法回滚 | 事务内全用 session |
| 🚨 事务内做 RPC 调用 | 长事务 + 锁竞争 | 事务只做 DB,RPC 提前执行 |
🚨 -table 大小写 |
MySQL 表名小写 | -table "user",不是 "User" |
🚨 修改 usermodel_gen.go |
goctl 会覆盖 | 自定义逻辑写到 usermodel.go |
| 🚨 批量查询走缓存 | sqlc 不支持 | 用 QueryRowsNoCacheCtx |
中间件与安全
| 陷阱 | 根因 | 解决 |
|---|---|---|
| 🚨 JWT leeway=0 | NTP 偏移 > 1s 拒绝合法 token | jwt.WithLeeway(5s) |
🚨 CORS * 上生产 |
安全漏洞 | 明确指定域名 |
| 🚨 中间件未在 ServiceContext 初始化 | 编译错误 | DSL 声明后需手动注册 |
| 🚨 panic 恢复跳过了限流 DeferFunc | 令牌未归还 | 在 recover handler 中手动清理 |
消息队列
| 陷阱 | 根因 | 解决 |
|---|---|---|
| 🚨 消费无幂等 | 重试导致重复处理 | 消息 ID + 去重表 |
| 🚨 消费失败不返回 error | kq 依赖 error 判断重试 | 处理失败 return err |
| 🚨 延迟队列用 Kafka | Kafka 不支持 | 用 asynq 或 Beanstalkd |
| 🚨 定时任务多实例重复 | 无分布式锁 | Redis SetNxEx |
| 🚨 asynq/payload 过大 | Redis 性能下降 | 仅传关键 ID,查库获取详情 |
运维部署
| 陷阱 | 根因 | 解决 |
|---|---|---|
| 🚨 生产镜像含源代码 | 安全风险 | 多阶段构建 |
🚨 terminationGracePeriod 太短 |
强制中断 | K8s 设置 ≥ 30s |
| 🚨 缺少 ReadinessProbe | 未就绪就接收流量 | 配置 TCP/HTTP 探测 |
| 🚨 日志写满容器磁盘 | 容器 rootfs 空间不足 | emptydir / sidecar 采集 |
| 🚨 无优雅停止监听器 | 资源未清理 | proc.AddShutdownListener() |
生产环境检查清单
上线前必检
- 配置检查:所有敏感信息使用环境变量或 Secret,禁止硬编码
- 超时设置:HTTP/RPC/DB/Redis 都设置了合理的超时值
- 连接池大小:根据负载测试调整
MaxPoolSize、MaxConns - 熔断降载:确认
Breaker: true、Shedding.CpuThreshold合理 - 日志格式:生产环境使用
Mode: file, Encoding: json - 链路追踪:配置
Telemetry,采样率根据 QPS 设置 - 健康检查:K8s 配置 ReadinessProbe 和 LivenessProbe
- 优雅停止:
terminationGracePeriodSeconds≥ 30s - 持久化存储:日志和临时文件使用 emptydir 或 PVC
上线后必看
- Prometheus 指标:QPS、延迟、错误率是否正常
- Jaeger 链路:是否有异常的耗时 span
- 熔断器:是否有异常打开(下游故障)
- 数据库连接池:是否有 wait_count > 0
- Redis 缓存命中率:是否有高 miss rate
- 消息队列积压:消费速度是否匹配产生速度
代码审查要点
| 关注点 | 检查内容 |
|---|---|
| ServiceContext | 所有依赖是否通过它注入?有没有在 Logic 里直接 new? |
| Context 传递 | 所有方法是否正确传递 ctx?logx.WithContext(ctx)? |
| 错误处理 | 是否用 CodeError + errors.Wrapf?是否区分业务/系统错误? |
| 事务边界 | 多 DB 操作是否在同一个事务内?是否避免了长事务? |
| RPC 超时 | 每个 RPC 调用是否设置了 context.WithTimeout? |
| 幂等性 | 消息消费、支付回调等是否做了幂等处理? |
| 硬编码 | 超时时间、重试次数、地址等是否从配置读取? |
社区标杆项目
| 项目 | GitHub | 推荐学习点 |
|---|---|---|
| go-zero-looklook | Mikaelemmmm/go-zero-looklook |
完整微服务体系(API+RPC+MQ+asynq) |
| zero-examples | zeromicro/zero-examples |
官方示例集 |
| go-zero-admin | lawrenceshao/go-zero-admin |
后台管理系统 |
| bookstore | zeromicro/bookstore |
最小完整示例 |
💡 建议深入研究
go-zero-looklook的代码结构,它是社区公认的 go-zero 最佳实践标杆项目。