如何加一个新服务
以加 order 为例(对标现有 product)。
0. 安装 goctl
go install github.com/zeromicro/go-zero/tools/goctl@latest
goctl --version
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
1. 创建 RPC 服务(goctl)
在仓库根目录执行:
goctl rpc new order
cd order
2. 将 proto 移到 proto/ 目录
goctl rpc new 默认把 proto 放在服务根目录(如 order/order.proto)。统一挪到 proto/:
mkdir proto
mv order.proto proto/
3. Zero 兼容 Nacos 等配置
编辑 order/internal/config/config.go:
package config
import "github.com/zeromicro/go-zero/zrpc"
type Config struct {
zrpc.RpcServerConf
Nacos NacosConf
Mysql MysqlConf
BizRedis RedisConf
AppLog LogConf
}
type NacosConf struct {
Hosts []string
NamespaceId string `json:",optional"`
Group string `json:",optional"`
RegisterIP string `json:",optional"`
}
type MysqlConf struct {
Host string
Port int
User string
Password string
Database string
Charset string `json:",default=utf8mb4"`
Prefix string `json:",optional"`
ReadHost string `json:",optional"`
ReadPort int `json:",optional"`
ReadUser string `json:",optional"`
ReadPassword string `json:",optional"`
ReadDatabase string `json:",optional"`
}
type RedisConf struct {
Host string
Port int `json:",default=6379"`
Password string `json:",optional"`
DB int `json:",optional"`
}
type LogConf struct {
Path string `json:",default=logs"`
InfoFile string `json:",default=info.log"`
ErrorFile string `json:",default=error.log"`
FatalFile string `json:",default=fatal.log"`
MaxSize int `json:",default=100"`
MaxBackups int `json:",default=10"`
MaxAge int `json:",default=30"`
}
4. 配置服务名与监听端口
编辑 order/etc/order.yaml:
Name: order-service
ListenOn: 0.0.0.0:10200
Mode: dev
Log:
ServiceName: order-service
Mode: file # console | file | volume
Encoding: plain
Level: info
Path: logs
KeepDays: 7
Compress: false
# Rotation: daily
# MaxSize: 100
# MaxBackups: 10
Nacos:
Hosts:
- rnacos:8848
NamespaceId: test
Group: LONE_SERVICES
RegisterIP: order
Mysql:
Host: mysql
Port: 3306
User: root
Password: "123123"
Database: dms-order
Charset: utf8mb4
Prefix:
# ReadHost: mysql-slave
# ReadPort: 3306
# ReadUser: root
# ReadPassword: "123123"
# ReadDatabase: dms-order
BizRedis:
Host: redis
Port: 6379
Password: ""
DB: 0
AppLog:
Path: logs
InfoFile: info.log
ErrorFile: error.log
FatalFile: fatal.log
MaxSize: 100
MaxBackups: 10
MaxAge: 30
5. 构建 mod
编辑 order/go.mod 增加本地包 pkg.local
go 1.26
replace pkg.local => ../pkg
6. 服务注册
编辑 order/order.go:
增加 import
"pkg.local/discovery"
"pkg.local/log"
"pkg.local/modelbase"
"pkg.local/mysql"
"pkg.local/redis"
"pkg.local/validate"
增加初始化
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
if err := log.Init(log.Config{
Path: c.AppLog.Path,
InfoFile: c.AppLog.InfoFile,
ErrorFile: c.AppLog.ErrorFile,
FatalFile: c.AppLog.FatalFile,
MaxSize: c.AppLog.MaxSize,
MaxBackups: c.AppLog.MaxBackups,
MaxAge: c.AppLog.MaxAge,
}); err != nil {
fmt.Fprintf(os.Stderr, "log init: %v\n", err)
os.Exit(1)
}
if err := discovery.Init(discovery.Config{
Hosts: c.Nacos.Hosts,
NamespaceId: c.Nacos.NamespaceId,
Group: c.Nacos.Group,
}); err != nil {
logx.Errorf("nacos init: %v", err)
}
registerIP := c.Nacos.RegisterIP
port, err := listenPort(c.ListenOn)
if err != nil {
logx.Errorf("parse ListenOn: %v", err)
}
if err := discovery.Register(discovery.Instance{
ServiceName: c.Name,
IP: registerIP,
Port: port,
Group: c.Nacos.Group,
}); err != nil {
logx.Errorf("nacos register: %v", err)
}
defer func() {
if err := discovery.Deregister(); err != nil {
logx.Errorf("nacos deregister: %v", err)
}
}()
db, err := mysql.New(mysql.Config{
Host: c.Mysql.Host,
Port: c.Mysql.Port,
User: c.Mysql.User,
Password: c.Mysql.Password,
Database: c.Mysql.Database,
Charset: c.Mysql.Charset,
Prefix: c.Mysql.Prefix,
ReadHost: c.Mysql.ReadHost,
ReadPort: c.Mysql.ReadPort,
ReadUser: c.Mysql.ReadUser,
ReadPassword: c.Mysql.ReadPassword,
ReadDatabase: c.Mysql.ReadDatabase,
})
if err != nil {
logx.Errorf("mysql init: %v", err)
}
if err := redis.Init(redis.Config{
Host: c.BizRedis.Host,
Port: c.BizRedis.Port,
Password: c.BizRedis.Password,
DB: c.BizRedis.DB,
}); err != nil {
logx.Errorf("redis init: %v", err)
}
modelbase.Init(db, modelbase.Config{Prefix: c.Mysql.Prefix})
ctx := svc.NewServiceContext(c, db)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
order.RegisterOrderServer(grpcServer, server.NewOrderServer(ctx))
if c.Mode == service.DevMode || c.Mode == service.TestMode {
reflection.Register(grpcServer)
}
})
defer s.Stop()
logx.AddWriter(logx.NewWriter(os.Stdout))
s.AddUnaryInterceptors(validate.UnaryServerInterceptor(validate.MustNew()))
logx.Infof("Starting rpc server at %s...", c.ListenOn)
s.Start()
}
func listenPort(listenOn string) (uint64, error) {
_, portStr, err := net.SplitHostPort(listenOn)
if err != nil {
return 0, err
}
return strconv.ParseUint(portStr, 10, 64)
}
执行 tidy
go mod tidy
编辑 internal/svc/servicecontext.go:
type ServiceContext struct {
Config config.Config
DB *gorm.DB
Prefix string
}
func NewServiceContext(c config.Config, db *gorm.DB) *ServiceContext {
return &ServiceContext{
Config: c,
DB: db,
Prefix: c.Mysql.Prefix,
}
}
7. 接入 Docker Compose
在 deploy/docker-compose.override.yml 增加服务:
order:
image: golang:1.26.5
volumes:
- ..:/src
- go-mod-cache:/go/pkg/mod
working_dir: /src/order
command: go run . -f etc/order.yaml
ports:
- "10200:10200"
environment:
NACOS_SERVER_ADDR: rnacos:8848
depends_on:
- rnacos
8. 生成 ProtoSet 给 BFF
protoc --include_imports --proto_path=proto --descriptor_set_out=../bff/etc/order.pb order.proto
9. 在 BFF 配 Upstream(HTTP 路由写在 proto)
在对应 RPC 上声明 google.api.http(需 import "google/api/annotations.proto";):
rpc Ping(Request) returns (Response) {
option (google.api.http) = {
post: "/admin/v3/order/ping"
body: "*"
};
}
编辑 bff/etc/bff.yaml,在 Upstreams 增加一段(不必写 Mappings,gateway 会从 ProtoSet 里的 http option 注册路由):
- Name: order
Grpc:
Target: order-service # 必须等于 order.yaml 的 Name
Timeout: 5000
ProtoSets:
- etc/order.pb
编写业务须知
只改业务逻辑(internal/logic/)不必跑 goctl。
改 proto/order.proto(新增/改名 RPC、改字段、改 http option 等)后,按文末「三条命令何时用」处理;
若 HTTP 路径有变,改 proto 里的 google.api.http 后重新跑命令 ② 生成 ProtoSet。
命令 ①:生成服务端 pb / grpc
goctl rpc protoc proto/admin.proto --proto_path=. --proto_path=../pkg/third_party --go_out=. --go-grpc_out=. --zrpc_out=.
什么时候跑:
- 只改 校验规则(必填/长度/范围等)→ 只跑这条
- 增删改字段、增删 RPC → 也要跑(服务端描述符要更新)
命令 ②:生成 BFF ProtoSet
protoc -I. -I../pkg/third_party --descriptor_set_out=../bff/etc/order.pb --include_imports proto/order.proto
什么时候跑:
- 增删改 字段(BFF gateway 编解码需要)
- 增删 RPC
- 改 google.api.http 路由
命令 ③:goctl 生成/更新脚手架
goctl rpc protoc proto/order.proto --go_out=. --go-grpc_out=. --zrpc_out=.
什么时候跑:
- 新增 RPC:需要生成
internal/logic、更新server/orderclient等 - 注意:可能覆盖已改过的
order.go等,生成后对比合并;也可用手写 logic/server 代替
Proto 常用校验规则(protovalidate)
文档:https://protovalidate.com/schemas/standard-rules/
依赖:import "buf/validate/validate.proto";,服务启动挂 pkg.local/validate 拦截器。
必填字符串
string name = 1 [(buf.validate.field).string = {min_len: 1, max_len: 256}];
min_len: 1 表示不能为空串。
可选字符串(仅限长度)
string subhead = 2 [(buf.validate.field).string = {max_len: 255}];
空串可通过;有内容时限制最大长度。
必填数值(含 0 合法)
double price = 6 [(buf.validate.field).double = {gte: 0}];
int32 period_validity = 16 [(buf.validate.field).int32 = {gte: 0}];
非必填数值(0 表示未传,跳过校验)
double store_price = 7 [(buf.validate.field) = {
ignore: IGNORE_IF_ZERO_VALUE,
double: {gte: 0}
}];
有值时仍要求 >= 0;未传/为 0 不校验。
枚举 / 固定取值(0 表示未传)
uint32 type = 19 [(buf.validate.field) = {
ignore: IGNORE_IF_ZERO_VALUE,
uint32: {in: [1, 2, 3]}
}];
uint32 sales_model = 4 [(buf.validate.field) = {
ignore: IGNORE_IF_ZERO_VALUE,
uint32: {in: [1, 2]}
}];
ID 必须大于 0
int64 id = 1 [(buf.validate.field).int64 = {gt: 0}];
无规则字段
uint32 number = 20; // 不做 protovalidate
删除字段编号(避免复用)
// reserved 2;
业务条件校验(如「普通商品必须带齐价格」)仍写在 internal/logic,不要全塞进 proto。