如果你已经熟悉 Gin 路由、GORM 查库,也听过 go-zero 的大名,那么这篇文章就是写给你的。
很多介绍 go-zero 的文章都会说:
- 它有代码生成
- 它内置微服务治理
- 它支持 zRPC
- 它有缓存、限流、熔断、链路追踪
- 它比 Gin + GORM 更工程化
但真正上手时,很多人还是会卡在一堆细节上:
.api文件怎么写?goctl怎么生成项目?- 配置怎么写?
- MySQL 和 Redis 怎么接?
- gRPC 服务怎么起?
- API 网关怎么调 RPC?
- 链路追踪怎么上报?
- 超时到底怎么传导?
- 熔断到底要不要自己配?
所以这篇文章不准备继续讲概念,而是直接带你串一个能跑通的最小完整 Demo。
读完并跑通之后,你会得到一个包含以下能力的 go-zero 项目:
- REST 路由:
/ping、/api/order/:id - gRPC 端点:
user.User/GetUser - MySQL 数据访问
- Redis 缓存
- 日志输出
- 链路追踪上报
- 超时链路传导
- 内置熔断能力
goctl自动生成 API / RPC / Model
也就是说,这篇文章不是“go-zero 是什么”,而是:
怎么把 go-zero 真正跑成一个可用工程。
一、为什么从 Gin + GORM 走向 go-zero?
如果你用 Gin + GORM 写过项目,一定很熟悉这种模式:
- 用 Gin 写路由和 Handler
- 用 GORM 做数据库访问
- 用 Viper 读配置
- 用中间件处理登录鉴权
- 用 Zap 或 Logrus 打日志
- 自己封装 HTTP Client 调别的系统
- 自己接 Jaeger、Consul、Prometheus、限流器、熔断器
这套方案很灵活,但问题也很明显:
所有稳定性能力都要自己拼。
比如:
- 超时传递
- 服务发现
- 熔断
- 限流
- 降载
- 链路追踪
- 日志规范
- 错误处理
- 工程目录规范
每一个都要自己选型、封装、踩坑。项目大了以后,团队协作成本会越来越高。
go-zero 的价值,不是取代 Gin,而是把这些工程能力尽量内置化、标准化。
一句话概括:
Gin + GORM 给你自由,go-zero 给你“少踩坑的自由”。
二、这个 Demo 最终长什么样?
我们会做两个服务:
order-api:REST 网关user-rpc:gRPC 用户服务
调用链如下:
客户端
│
▼
order-api (REST)
│ GET /ping
│ GET /api/order/:id
│
│ zrpc client
▼
user-rpc (gRPC)
│
├── MySQL
└── Redis Cache
最终暴露的能力:
| 类型 | 端点 | 说明 |
|---|---|---|
| REST | GET /ping |
健康检查 |
| REST | GET /api/order/:id |
订单详情,内部调用用户服务 |
| gRPC | user.User/GetUser |
查询用户,走 MySQL + Redis 缓存 |
这个结构非常典型:
外层一个 API 网关,内部一个 RPC 服务,再通过 MySQL + Redis 提供数据访问。
三、项目目录结构
先约定一下项目结构。后面所有命令都以项目根目录为准。
最终目录大致如下:
demo/
├── docker-compose.yaml
├── go.mod
├── api/
│ └── order.api
├── proto/
│ └── user.proto
├── sql/
│ ├── user.sql
│ └── user-data.sql
├── user-rpc/
│ ├── etc/
│ │ └── user-rpc.yaml
│ ├── internal/
│ │ ├── config/
│ │ ├── logic/
│ │ ├── model/
│ │ ├── server/
│ │ └── svc/
│ ├── user/
│ └── user.go
└── order-api/
├── etc/
│ └── order-api.yaml
├── internal/
│ ├── config/
│ ├── handler/
│ ├── logic/
│ ├── middleware/
│ ├── svc/
│ └── types/
└── order.go
不用一开始就把每个文件都背下来。
你只需要知道:
api/放 REST 接口的 DSLproto/放 gRPC 的 protosql/放建表语句user-rpc/是 RPC 服务order-api/是 REST 服务
四、准备基础设施:MySQL、Redis、etcd、Jaeger
为了尽量减少环境差异,我们先用 Docker 把依赖都跑起来。
1. docker-compose.yaml
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: demo
ports:
- "3306:3306"
volumes:
- ./sql:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-ppassword"]
interval: 5s
timeout: 5s
retries: 30
redis:
image: redis:7
ports:
- "6379:6379"
etcd:
image: quay.io/coreos/etcd:v3.5.13
command: etcd --advertise-client-urls=http://localhost:2379 --listen-client-urls=http://0.0.0.0:2379
ports:
- "2379:2379"
jaeger:
image: jaegertracing/all-in-one:1.57
ports:
- "16686:16686"
- "14268:14268"
这里用了四个组件:
mysql:业务数据库redis:缓存etcd:服务注册与发现jaeger:链路追踪界面
启动:
docker compose up -d
五、准备数据库初始化 SQL
1. sql/user.sql
这个文件后面会被 goctl model 使用。
CREATE TABLE IF NOT EXISTS user (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. sql/user-data.sql
初始化测试数据:
INSERT INTO user (id, name) VALUES (1, 'Tom'), (2, 'Jerry')
ON DUPLICATE KEY UPDATE name = VALUES(name);
如果你用 Docker 启动,MySQL 初始化时会自动执行 sql/ 目录下的脚本。
六、初始化项目与安装工具
在项目根目录执行:
go mod init demo
go get -u github.com/zeromicro/go-zero@latest
go install github.com/zeromicro/go-zero/tools/goctl@latest
检查 goctl:
goctl --version
如果你要写 gRPC,还需要 protoc。macOS 可以这样安装:
brew install protobuf
goctl env check --install --verbose --force
到这里,工具链就准备好了。
七、先写 Proto:定义 gRPC 服务
我们创建一个 proto/user.proto 文件。
proto/user.proto
syntax = "proto3";
package user;
option go_package = "./user";
message GetUserReq {
int64 id = 1;
}
message GetUserResp {
int64 id = 1;
string name = 2;
}
service User {
rpc GetUser(GetUserReq) returns (GetUserResp);
}
这个文件定义了:
- 请求参数:
GetUserReq - 响应结果:
GetUserResp - 服务名:
User - 方法名:
GetUser
后面会用 goctl 基于它生成 RPC 工程。
八、再写 API DSL:定义 REST 路由
go-zero 的 REST 服务通常不是手写路由,而是先写 .api 文件。
我们创建 api/order.api。
api/order.api
syntax = "v1"
info(
title: "order-api"
desc: "订单网关示例"
author: "you"
)
type PingResp {
Msg string `json:"msg"`
}
type OrderInfoReq {
Id int64 `path:"id"`
}
type OrderInfoResp {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
UserName string `json:"user_name"`
Amount int64 `json:"amount"`
}
service order-api {
@handler PingHandler
get /ping returns (PingResp)
}
@server(
middleware: AuthMiddleware
)
service order-api {
@handler OrderInfoHandler
get /api/order/:id (OrderInfoReq) returns (OrderInfoResp)
}
这里有两个接口:
/ping
不需要鉴权,用于健康检查。/api/order/:id
需要鉴权,走AuthMiddleware。
如果你熟悉 Gin,可以把 .api 文件理解为:
把路由、参数结构体、响应结构体提前声明出来,然后让工具帮你生成样板代码。
九、用 goctl 生成工程骨架
接下来是 go-zero 非常核心的一步:代码生成。
在项目根目录执行以下命令。
1. 生成 user-rpc 工程
rm -rf user-rpc order-api
mkdir -p user-rpc
cp proto/user.proto user-rpc/user.proto
cd user-rpc
goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. -style goZero
cd ..
2. 生成 order-api 工程
goctl api go -api api/order.api -dir order-api -style goZero
3. 生成带缓存的 MySQL Model
goctl model mysql ddl -src sql/user.sql -dir user-rpc/internal/model -c
重点说一下第三个命令里的 -c。
-c 表示生成带缓存的 Model。
这是 go-zero 相比很多传统 ORM 很有特色的一个能力:
它会把 Cache-Aside 模式直接固化到生成代码里。
也就是说:
- 查询时先查 Redis
- Redis 没有再查 MySQL
- 查完 MySQL 后回写 Redis
- 更新或删除时自动清理缓存
这些逻辑不用你自己在业务层反复手写。
十、完善 user-rpc:数据库、缓存、日志、链路追踪
生成之后,我们开始补业务代码。
1. 配置文件:user-rpc/etc/user-rpc.yaml
Name: user-rpc
ListenOn: 0.0.0.0:9001
Timeout: 3000
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Mysql:
DataSource: root:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local
CacheRedis:
- Host: 127.0.0.1:6379
Type: node
Telemetry:
Name: user-rpc
Endpoint: http://127.0.0.1:14268/api/traces
Sampler: 1.0
Batcher: jaeger
Log:
Mode: console
Level: info
这份配置里包含了几类关键信息:
ListenOn:RPC 监听地址Etcd:服务注册与发现Mysql:数据库连接CacheRedis:缓存配置Telemetry:链路追踪Log:日志输出
2. 配置结构体:user-rpc/internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
zrpc.RpcServerConf
Mysql struct {
DataSource string
}
CacheRedis cache.CacheConf
}
这里的结构很直观:
zrpc.RpcServerConf:RPC 服务基础配置Mysql:自定义数据库配置CacheRedis:缓存配置
3. 依赖注入:user-rpc/internal/svc/servicecontext.go
package svc
import (
"demo/user-rpc/internal/config"
"demo/user-rpc/internal/model"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
conn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(conn, c.CacheRedis),
}
}
如果你写过 Gin,可以把 ServiceContext 理解成:
一个统一存放依赖的容器。
你可以把它类比成:
- Gin 项目里的
db - 项目里全局注入的
service - 或者依赖注入框架里的
container
但在 go-zero 里,它就是一个显式传递的结构体。
4. 业务逻辑:user-rpc/internal/logic/getuserlogic.go
package logic
import (
"context"
"time"
"demo/user-rpc/internal/model"
"demo/user-rpc/internal/svc"
"demo/user-rpc/user"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type GetUserLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
return &GetUserLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *GetUserLogic) GetUser(in *user.GetUserReq) (*user.GetUserResp, error) {
// 演示超时传导:
// 当访问 id=999 时,user-rpc 故意等 3 秒。
// 而 order-api 的 RPC 客户端超时设置为 2 秒,
// 因此上游会先收到 DeadlineExceeded。
if in.Id == 999 {
select {
case <-time.After(3 * time.Second):
case <-l.ctx.Done():
l.Infof("user rpc context done: %v", l.ctx.Err())
return nil, status.Error(codes.DeadlineExceeded, "user rpc timeout demo")
}
}
data, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
if err != nil {
if err == model.ErrNotFound {
l.Errorf("user id=%d not found", in.Id)
return nil, status.Errorf(codes.NotFound, "user %d not found", in.Id)
}
l.Errorf("find user id=%d error: %v", in.Id, err)
return nil, status.Error(codes.Internal, "query user failed")
}
l.Infof("get user from cache/db id=%d name=%s", data.Id, data.Name)
return &user.GetUserResp{
Id: data.Id,
Name: data.Name,
}, nil
}
这里有几个关键点:
l.svcCtx.UserModel.FindOne- 这是
goctl model生成的查询方法 - 自带缓存逻辑
- 这是
logx- go-zero 的日志组件
- 会自动带上上下文信息
status.Error- 返回标准 gRPC 错误
- 后续网关可以把这些错误映射成 HTTP 状态码
id=999的演示逻辑- 用于演示超时传导
- 后面会专门验证它
5. 服务入口:user-rpc/user.go
package main
import (
"flag"
"fmt"
"demo/user-rpc/internal/config"
"demo/user-rpc/internal/server"
"demo/user-rpc/internal/svc"
"demo/user-rpc/user"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
var configFile = flag.String("f", "etc/user-rpc.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
user.RegisterUserServer(grpcServer, server.NewUserServer(ctx))
reflection.Register(grpcServer)
})
defer s.Stop()
fmt.Printf("user rpc server is running at %s\n", c.ListenOn)
s.Start()
}
到这里,user-rpc 就完整了。
它包含:
- 配置
- 数据库
- 缓存
- 日志
- gRPC 服务
- 链路追踪上报
- 超时处理示例
十一、完善 order-api:路由、中间件、RPC 调用、错误映射
接下来写网关层。
1. 配置文件:order-api/etc/order-api.yaml
Name: order-api
Host: 0.0.0.0
Port: 8888
Timeout: 3000
MaxBytes: 1048576
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Timeout: 2000
NonBlock: true
Telemetry:
Name: order-api
Endpoint: http://127.0.0.1:14268/api/traces
Sampler: 1.0
Batcher: jaeger
Log:
Mode: console
Level: info
这里最重要的是超时配置:
order-api自身超时:3000ms- 调
user-rpc超时:2000ms
这会在后面形成一个非常典型的超时传导链路。
2. 配置结构体:order-api/internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf
}
3. 依赖注入:order-api/internal/svc/servicecontext.go
这里为了降低跨服务生成代码的耦合,我们直接使用:
zrpc.MustNewClientuser.NewUserClient
而不是强依赖 goctl 生成的 userclient 包。
这样做的好处是:
- 仍然保留 go-zero 的服务发现、负载均衡、超时、熔断、链路追踪能力
- 代码更容易跑通,也更容易理解
package svc
import (
"demo/order-api/internal/config"
"demo/user-rpc/user"
"github.com/zeromicro/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
UserRpc user.UserClient
UserRpcConn zrpc.Client
}
func NewServiceContext(c config.Config) *ServiceContext {
userRpcConn := zrpc.MustNewClient(c.UserRpc)
return &ServiceContext{
Config: c,
UserRpc: user.NewUserClient(userRpcConn.Conn()),
UserRpcConn: userRpcConn,
}
}
4. Ping 逻辑:order-api/internal/logic/pinglogic.go
package logic
import (
"context"
"demo/order-api/internal/svc"
"demo/order-api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type PingLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic {
return &PingLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *PingLogic) Ping() (*types.PingResp, error) {
return &types.PingResp{
Msg: "pong",
}, nil
}
5. 订单逻辑:order-api/internal/logic/orderinfologic.go
package logic
import (
"context"
"demo/order-api/internal/svc"
"demo/order-api/internal/types"
"demo/user-rpc/user"
"github.com/zeromicro/go-zero/core/logx"
)
type OrderInfoLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewOrderInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderInfoLogic {
return &OrderInfoLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (*types.OrderInfoResp, error) {
var userID int64
switch req.Id {
case 999:
// 用于演示超时传导
userID = 999
case 404:
// 用于演示 gRPC NotFound
userID = 404
default:
// 简单 mock:订单 1 -> 用户 2,订单 2 -> 用户 1,依此类推
userID = req.Id%2 + 1
}
resp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.GetUserReq{
Id: userID,
})
if err != nil {
l.Errorf("call user rpc GetUser id=%d error: %v", userID, err)
return nil, err
}
return &types.OrderInfoResp{
Id: req.Id,
UserId: resp.Id,
UserName: resp.Name,
Amount: 9900,
}, nil
}
这段代码模拟的是:
- 根据订单 ID 查用户
- 再从用户服务拿用户信息
- 最后拼订单详情返回
6. 中间件:order-api/internal/middleware/authmiddleware.go
如果 goctl 已经生成了这个文件,直接替换成下面的实现。
package middleware
import (
"net/http"
)
type AuthMiddleware struct{}
func NewAuthMiddleware() *AuthMiddleware {
return &AuthMiddleware{}
}
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer demo-token" {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
return
}
next(w, r)
}
}
这个中间件很简单:
- 只允许
Authorization: Bearer demo-token - 否则返回 401
7. 服务入口:order-api/order.go
package main
import (
"flag"
"fmt"
"net/http"
"demo/order-api/internal/config"
"demo/order-api/internal/handler"
"demo/order-api/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var configFile = flag.String("f", "etc/order-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
httpx.SetErrorHandler(func(err error) (int, any) {
if s, ok := status.FromError(err); ok {
switch s.Code() {
case codes.NotFound:
return http.StatusNotFound, map[string]string{
"error": s.Message(),
}
case codes.DeadlineExceeded:
return http.StatusGatewayTimeout, map[string]string{
"error": "upstream timeout",
}
default:
return http.StatusInternalServerError, map[string]string{
"error": s.Message(),
}
}
}
return http.StatusBadRequest, map[string]string{
"error": err.Error(),
}
})
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("order api is running at %s:%d\n", c.Host, c.Port)
server.Start()
}
这里特意加了一个统一错误处理:
codes.NotFound-> HTTP 404codes.DeadlineExceeded-> HTTP 504
这样前端拿到的错误会更友好。
十二、整理依赖
在项目根目录执行:
go mod tidy
如果你发现 user-rpc/userclient 目录里的生成代码有 import 路径问题,可以先删掉它:
rm -rf user-rpc/userclient
因为本文网关调用部分用的是:
zrpc.MustNewClientuser.NewUserClient(...)
所以不依赖 userclient 也能完整跑通。
十三、启动服务
建议开两个终端。
1. 启动 user-rpc
cd user-rpc
go run user.go -f etc/user-rpc.yaml
正常会看到:
user rpc server is running at 0.0.0.0:9001
2. 启动 order-api
cd order-api
go run order.go -f etc/order-api.yaml
正常会看到:
order api is running at 0.0.0.0:8888
十四、验证接口
1. 健康检查
curl -i http://127.0.0.1:8888/ping
返回:
{"msg":"pong"}
2. 不带 token 访问受保护接口
curl -i http://127.0.0.1:8888/api/order/1
返回:
{"error":"unauthorized"}
HTTP 状态码为 401。
3. 带 token 访问订单详情
curl -i -H "Authorization: Bearer demo-token" http://127.0.0.1:8888/api/order/1
返回类似:
{
"id": 1,
"user_id": 2,
"user_name": "Jerry",
"amount": 9900
}
再试:
curl -i -H "Authorization: Bearer demo-token" http://127.0.0.1:8888/api/order/2
返回类似:
{
"id": 2,
"user_id": 1,
"user_name": "Tom",
"amount": 9900
}
4. 模拟用户不存在
curl -i -H "Authorization: Bearer demo-token" http://127.0.0.1:8888/api/order/404
返回:
{
"error": "user 404 not found"
}
HTTP 状态码为 404。
5. 模拟超时传导
curl -i -H "Authorization: Bearer demo-token" http://127.0.0.1:8888/api/order/999
返回:
{
"error": "upstream timeout"
}
HTTP 状态码为 504。
这个例子说明:
order-api总超时是 3 秒- 调
user-rpc超时是 2 秒 user-rpc故意等 3 秒- 最终上游先超时,而不是无限等待
这就是非常典型的链路超时传导。
十五、验证缓存是否生效
第一次请求某个用户时,通常会查 MySQL。
再次请求同一个用户时,通常会走 Redis。
你可以用下面的命令简单看看缓存 key:
redis-cli --scan --pattern '*user*'
不同版本生成代码的 key 格式可能略有差异,但一般会包含表名和主键,例如类似:
cache:user:1
与此同时,你会在 user-rpc 的日志里看到类似:
get user from cache/db id=1 name=Tom
这说明数据库、缓存、日志这三块已经串起来了。
十六、验证链路追踪
打开 Jaeger:
http://127.0.0.1:16686
然后访问几次接口,例如:
curl -H "Authorization: Bearer demo-token" http://127.0.0.1:8888/api/order/1
在 Jaeger 中,你通常可以看到:
order-api的 HTTP spanuser-rpc的 gRPC span- 同一条调用链被串起来
这说明两件事已经生效:
- 链路追踪上报成功
- 跨服务上下文传导成功
这也是很多 Gin 项目要自己埋点才能实现的部分。
十七、关于熔断、限流、降载,你该怎么理解?
很多文章一讲到这里,就会开始堆概念。
但在这个 Demo 里,你可以换一个更实际的角度理解。
1. 熔断
go-zero 的熔断能力是内置在调用链里的,尤其是在 RPC 调用中默认生效。
它不是你手工 new 一个 breaker 实例,再去包装每个调用。
在这个 Demo 里,如果你想感受熔断,可以反复请求:
curl -H "Authorization: Bearer demo-token" http://127.0.0.1:8888/api/order/999
制造大量超时或失败,然后观察客户端行为。
重点不是“自己写一个熔断器”,而是:
稳定性能力被框架默认提供了。
2. 限流与降载
本文 Demo 没有单独展开限流配置,但 go-zero 本身提供:
- 自适应降载
- 限流能力
- 请求体积限制
- 超时控制
比如 order-api.yaml 中的:
MaxBytes: 1048576
就是非常典型的入口保护配置。
十八、这个 Demo 到底串通了哪些能力?
到这里,你已经不是“知道 go-zero 有哪些特性”,而是真的把它们跑起来了。
我们回顾一下:
1. 配置管理
yamlconf.MustLoad- 强类型结构体绑定
2. 自动代码生成
goctl api gogoctl rpc protocgoctl model mysql ddl -c
3. 路由
.api文件声明路由- 自动生成
handler/routes
4. 数据库
goctl modelsqlx- MySQL CRUD
5. 缓存
- 自动生成 Cache-Aside 逻辑
- 查询、回写、失效都由生成代码处理
6. 日志
logx- access log
- logic 层日志
7. gRPC + proto
- proto 定义服务
goctl rpc protoc生成 RPC 工程骨架
8. 熔断
- zrpc 调用链内置治理能力
- 不需要手工拼第三方熔断库
9. 链路追踪上报
Telemetry- Jaeger 查看调用链
10. 链路传导
context一路传递- HTTP -> RPC -> MySQL/Redis
- 超时时间逐级传导
十九、如果你正准备从 Gin + GORM 迁移,我的建议是渐进式
不建议一上来推倒重来。
更稳的路径是这样的:
第一步:新模块先试点
新功能模块用 go-zero 的 .api + goctl model 写一遍。
先让团队熟悉:
.api语法- 生成目录结构
logic / handler / svc分层
第二步:网关先收敛
用 go-zero 的 rest.Server 做统一 API 网关。
老服务可以先挂在网关后面,逐步改造。
第三步:数据层平滑迁移
ServiceContext 里可以同时放:
*gorm.DBgoctl model生成的 Model
也就是说,GORM 和 go-zero 的数据访问方式可以共存。
第四步:治理能力逐步接入
先上默认能力:
- 超时
- 熔断
- 降载
再做:
- 服务发现
- RPC 化
- 链路追踪
这样迁移成本最低。
二十、最后总结一下
如果这篇文章你只记一句话,那就是:
Gin + GORM 给你自由,go-zero 给你“少踩坑的自由”。
Gin 很轻,GORM 很顺手,但在真实生产环境里,你迟早要补上这些能力:
- 配置管理
- 日志规范
- 超时传递
- 服务发现
- 熔断限流
- 链路追踪
- 统一工程结构
go-zero 的价值,就是把这些东西尽量变成默认能力,而不是让你每次都从零拼装。
而这篇文章给你的,不是一组孤立概念,而是一个完整闭环:
- 有路由
- 有 RPC
- 有数据库
- 有缓存
- 有日志
- 有链路追踪
- 有超时传导
- 有熔断入口
只要你把这个 Demo 跑通一遍,再回头去看 go-zero 官方文档,理解会完全不一样。
附:本文涉及的核心命令速查
# API 服务生成
goctl api go -api order.api -dir order-api -style goZero
# RPC 服务生成
goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. -style goZero
# 带缓存 Model 生成
goctl model mysql ddl -src sql/user.sql -dir user-rpc/internal/model -c
# 环境检查与安装
goctl env check --install --verbose --force
# 整理依赖
go mod tidy