Skip to content

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
└── Makefile

2. 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. 服务间通信

订单服务创建订单时,需要:

  1. 调用 product-rpc 查商品、扣库存
  2. 调用 user-rpc 查用户信息
  3. 写入订单
  4. 调用 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-Aside30 分钟
商品库存不缓存(强一致性)-
订单列表不缓存(实时性要求高)-
订单详情短期缓存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 ~/.goctl

2. 团队协作的 .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.v1package 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.yaml

2. 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 production

3. 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 production

4. 监控告警

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 数据库与 Redissqlx、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 微服务系统。祝编程愉快!