From 173f76acf98bcb5072f0a9bee48848a45a13bc9d Mon Sep 17 00:00:00 2001 From: Kevin Wan Date: Sat, 24 Jan 2026 19:47:14 +0800 Subject: [PATCH] feat: add cmdline argument to control whether generate package name from proto filename (#5387) --- tools/goctl/go.mod | 8 +- tools/goctl/go.sum | 12 +- tools/goctl/rpc/README.md | 51 +++++--- tools/goctl/rpc/cli/cli.go | 4 + tools/goctl/rpc/cli/zrpc.go | 1 + tools/goctl/rpc/cmd.go | 2 + tools/goctl/rpc/generator/gen.go | 3 + tools/goctl/rpc/generator/mkdir.go | 17 ++- tools/goctl/rpc/generator/mkdir_test.go | 147 ++++++++++++++++++++++++ 9 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 tools/goctl/rpc/generator/mkdir_test.go diff --git a/tools/goctl/go.mod b/tools/goctl/go.mod index 699336ea8..8adb9ee69 100644 --- a/tools/goctl/go.mod +++ b/tools/goctl/go.mod @@ -1,13 +1,13 @@ module github.com/zeromicro/go-zero/tools/goctl -go 1.21 +go 1.23 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/emicklei/proto v1.14.2 github.com/fatih/structtag v1.2.0 github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e - github.com/go-sql-driver/mysql v1.9.0 + github.com/go-sql-driver/mysql v1.9.3 github.com/gookit/color v1.6.0 github.com/iancoleman/strcase v0.3.0 github.com/spf13/cobra v1.10.2 @@ -19,7 +19,7 @@ require ( github.com/zeromicro/go-zero v1.9.4 golang.org/x/text v0.22.0 google.golang.org/grpc v1.65.0 - google.golang.org/protobuf v1.36.5 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 ) @@ -44,7 +44,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/pyroscope-go v1.2.7 // indirect diff --git a/tools/goctl/go.sum b/tools/goctl/go.sum index 4a8efe4ee..e24672804 100644 --- a/tools/goctl/go.sum +++ b/tools/goctl/go.sum @@ -50,8 +50,8 @@ github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e h1:auobAirzhPsL github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e/go.mod h1:NAKTe9SplQBxIUlHlsuId1jk1I7bWTVV/2q/GtdRi6g= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= -github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -62,8 +62,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -281,8 +281,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/tools/goctl/rpc/README.md b/tools/goctl/rpc/README.md index 2394fbd1b..2073255f6 100644 --- a/tools/goctl/rpc/README.md +++ b/tools/goctl/rpc/README.md @@ -53,7 +53,7 @@ Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持prot ```Bash $ goctl rpc template -o=user.proto ``` - + ```proto syntax = "proto3"; @@ -72,7 +72,7 @@ service User { rpc Ping(Request) returns(Response); } ``` - + * 生成rpc服务代码 @@ -96,15 +96,16 @@ Examples: goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=. Flags: - --branch string The branch of the remote repo, it does work with --remote - -h, --help help for protoc - --home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority - -m, --multiple Generated in multiple rpc service mode - --remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority - The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure - --style string The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] (default "gozero") - -v, --verbose Enable log output - --zrpc_out string The zrpc output directory + --branch string The branch of the remote repo, it does work with --remote + -h, --help help for protoc + --home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority + -m, --multiple Generated in multiple rpc service mode + --name-from-filename Use proto filename instead of package name for service naming (legacy behavior) + --remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority + The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure + --style string The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md] (default "gozero") + -v, --verbose Enable log output + --zrpc_out string The zrpc output directory ``` ### 参数说明 @@ -112,19 +113,43 @@ Flags: * --branch 指定远程仓库模板分支 * --home 指定goctl模板根目录 * -m, --multiple 指定生成多个rpc服务模式, 默认为 false, 如果为 false, 则只支持生成一个rpc service, 如果为 true, 则支持生成多个 rpc service,且多个 rpc service 会分组。 +* --name-from-filename 使用proto文件名而非package名称来命名服务(旧版行为)。默认使用package名称,这样可以支持多个proto文件共享同一个package。 * --style 指定文件输出格式 * -v, --verbose 显示日志 * --zrpc_out 指定zrpc输出目录 > ## --multiple > 是否开启多个 rpc service 生成,如果开启,则满足一下新特性 -> 1. 支持 1 到多个 rpc service +> 1. 支持 1 到多个 rpc service > 2. 生成 rpc 服务会按照服务名称分组(尽管只有一个 rpc service) > 3. rpc client 的文件目录变更为固定名称 `client` -> +> > 如果不开启,则和旧版本 rpc 生成逻辑一样(兼容) > 1. 有且只能有一个 rpc service +> ## Service Naming (Multi-Proto File Support) +> +> By default, the service name is derived from the **proto package name** (e.g., `package user;` → service name `user`). +> This enables splitting a large proto file into multiple smaller files that share the same package name, +> which is particularly useful for AI-assisted development where smaller files are easier to process. +> +> **Example: Multiple proto files with same package** +> ``` +> protos/ +> ├── user_base.proto # package user; +> ├── user_auth.proto # package user; +> └── user_profile.proto # package user; +> ``` +> All three files will generate into a single `user` service. +> +> **Legacy behavior (--name-from-filename)** +> +> If you need the old behavior where service name is derived from the proto filename, +> use the `--name-from-filename` flag: +> ```bash +> goctl rpc protoc user.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=. --name-from-filename +> ``` + ## rpc 服务生成 example 详情见 [example/rpc](https://github.com/zeromicro/go-zero/tree/master/tools/goctl/example) diff --git a/tools/goctl/rpc/cli/cli.go b/tools/goctl/rpc/cli/cli.go index 09193052a..81f146a8d 100644 --- a/tools/goctl/rpc/cli/cli.go +++ b/tools/goctl/rpc/cli/cli.go @@ -48,6 +48,9 @@ var ( VarBoolClient bool // VarStringModule describes the module name for go.mod. VarStringModule string + // VarBoolNameFromFilename describes whether to derive service name from proto filename + // instead of the proto package name. Default is false (uses package name). + VarBoolNameFromFilename bool ) // RPCNew is to generate rpc greet service, this greet service can speed @@ -94,6 +97,7 @@ func RPCNew(_ *cobra.Command, args []string) error { ctx.ProtocCmd = fmt.Sprintf("protoc -I=%s %s --go_out=%s --go-grpc_out=%s", filepath.Dir(src), filepath.Base(src), filepath.Dir(src), filepath.Dir(src)) ctx.IsGenClient = VarBoolClient ctx.Module = VarStringModule + ctx.NameFromFilename = VarBoolNameFromFilename grpcOptList := VarStringSliceGoGRPCOpt if len(grpcOptList) > 0 { diff --git a/tools/goctl/rpc/cli/zrpc.go b/tools/goctl/rpc/cli/zrpc.go index 3f42c1135..41f906ff7 100644 --- a/tools/goctl/rpc/cli/zrpc.go +++ b/tools/goctl/rpc/cli/zrpc.go @@ -104,6 +104,7 @@ func ZRPC(_ *cobra.Command, args []string) error { ctx.ProtocCmd = strings.Join(protocArgs, " ") ctx.IsGenClient = VarBoolClient ctx.Module = VarStringModule + ctx.NameFromFilename = VarBoolNameFromFilename g := generator.NewGenerator(style, verbose) return g.Generate(&ctx) } diff --git a/tools/goctl/rpc/cmd.go b/tools/goctl/rpc/cmd.go index c474acc2c..51c6fedeb 100644 --- a/tools/goctl/rpc/cmd.go +++ b/tools/goctl/rpc/cmd.go @@ -42,6 +42,7 @@ func init() { newCmdFlags.StringVar(&cli.VarStringBranch, "branch") newCmdFlags.StringVar(&cli.VarStringModule, "module") newCmdFlags.BoolVarP(&cli.VarBoolVerbose, "verbose", "v") + newCmdFlags.BoolVar(&cli.VarBoolNameFromFilename, "name-from-filename") newCmdFlags.MarkHidden("go_opt") newCmdFlags.MarkHidden("go-grpc_opt") newCmdFlags.BoolVarPWithDefaultValue(&cli.VarBoolClient, "client", "c", true) @@ -60,6 +61,7 @@ func init() { protocCmdFlags.StringVar(&cli.VarStringBranch, "branch") protocCmdFlags.StringVar(&cli.VarStringModule, "module") protocCmdFlags.BoolVarP(&cli.VarBoolVerbose, "verbose", "v") + protocCmdFlags.BoolVar(&cli.VarBoolNameFromFilename, "name-from-filename") protocCmdFlags.MarkHidden("go_out") protocCmdFlags.MarkHidden("go-grpc_out") protocCmdFlags.MarkHidden("go_opt") diff --git a/tools/goctl/rpc/generator/gen.go b/tools/goctl/rpc/generator/gen.go index 9ff9aa60d..262d72a45 100644 --- a/tools/goctl/rpc/generator/gen.go +++ b/tools/goctl/rpc/generator/gen.go @@ -32,6 +32,9 @@ type ZRpcContext struct { IsGenClient bool // Module is the custom module name for go.mod Module string + // NameFromFilename uses proto filename instead of package name for service naming. + // Default is false (uses package name, which supports multi-proto files). + NameFromFilename bool } // Generate generates a rpc service, through the proto file, diff --git a/tools/goctl/rpc/generator/mkdir.go b/tools/goctl/rpc/generator/mkdir.go index 5ee314d00..c370e32cd 100644 --- a/tools/goctl/rpc/generator/mkdir.go +++ b/tools/goctl/rpc/generator/mkdir.go @@ -199,7 +199,9 @@ func mkdir(ctx *ctx.ProjectContext, proto parser.Proto, conf *conf.Config, c *ZR return nil, err } } - serviceName := proto.Package.Name + + serviceName := determineServiceName(proto, c) + return &defaultDirContext{ ctx: ctx, inner: inner, @@ -270,3 +272,16 @@ func (d *defaultDirContext) GetServiceName() stringx.String { func (d *Dir) Valid() bool { return len(d.Filename) > 0 && len(d.Package) > 0 } + +// determineServiceName returns the service name based on the proto file and context. +// By default, it uses the proto package name (supports multi-proto files). +// Falls back to filename if --name-from-filename flag is set or package name is empty. +func determineServiceName(proto parser.Proto, c *ZRpcContext) string { + if c != nil && c.NameFromFilename { + return strings.TrimSuffix(proto.Name, filepath.Ext(proto.Name)) + } + if proto.Package.Package != nil && len(proto.Package.Name) > 0 { + return proto.Package.Name + } + return strings.TrimSuffix(proto.Name, filepath.Ext(proto.Name)) +} diff --git a/tools/goctl/rpc/generator/mkdir_test.go b/tools/goctl/rpc/generator/mkdir_test.go new file mode 100644 index 000000000..5de9d70e6 --- /dev/null +++ b/tools/goctl/rpc/generator/mkdir_test.go @@ -0,0 +1,147 @@ +package generator + +import ( + "testing" + + "github.com/emicklei/proto" + "github.com/stretchr/testify/assert" + "github.com/zeromicro/go-zero/tools/goctl/rpc/parser" +) + +func TestServiceNameDetermination(t *testing.T) { + tests := []struct { + name string + protoName string + packageName string + hasPackage bool + nameFromFilename bool + expectedName string + }{ + { + name: "default uses package name when available", + protoName: "user.proto", + packageName: "userservice", + hasPackage: true, + nameFromFilename: false, + expectedName: "userservice", + }, + { + name: "flag enabled uses filename instead of package", + protoName: "user.proto", + packageName: "userservice", + hasPackage: true, + nameFromFilename: true, + expectedName: "user", + }, + { + name: "fallback to filename when package is empty", + protoName: "order.proto", + packageName: "", + hasPackage: true, + nameFromFilename: false, + expectedName: "order", + }, + { + name: "fallback to filename when package is nil", + protoName: "product.proto", + packageName: "", + hasPackage: false, + nameFromFilename: false, + expectedName: "product", + }, + { + name: "flag enabled with nil package uses filename", + protoName: "catalog.proto", + packageName: "", + hasPackage: false, + nameFromFilename: true, + expectedName: "catalog", + }, + { + name: "handles proto file with complex name", + protoName: "user-service.proto", + packageName: "user", + hasPackage: true, + nameFromFilename: false, + expectedName: "user", + }, + { + name: "handles proto file with complex name and flag", + protoName: "user-service.proto", + packageName: "user", + hasPackage: true, + nameFromFilename: true, + expectedName: "user-service", + }, + { + name: "nil context uses package name", + protoName: "account.proto", + packageName: "accountpb", + hasPackage: true, + nameFromFilename: false, + expectedName: "accountpb", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build the proto struct + p := parser.Proto{ + Name: tt.protoName, + } + if tt.hasPackage { + p.Package = parser.Package{ + Package: &proto.Package{ + Name: tt.packageName, + }, + } + } + + // Build the context + var ctx *ZRpcContext + if tt.name != "nil context uses package name" { + ctx = &ZRpcContext{ + NameFromFilename: tt.nameFromFilename, + } + } + + // Call the helper function to determine service name + serviceName := determineServiceName(p, ctx) + assert.Equal(t, tt.expectedName, serviceName) + }) + } +} + +func TestServiceNameWithNilContext(t *testing.T) { + p := parser.Proto{ + Name: "test.proto", + Package: parser.Package{ + Package: &proto.Package{ + Name: "testpkg", + }, + }, + } + + // nil context should use package name + serviceName := determineServiceName(p, nil) + assert.Equal(t, "testpkg", serviceName) +} + +func TestServiceNameFallbackWithEmptyPackage(t *testing.T) { + p := parser.Proto{ + Name: "myservice.proto", + Package: parser.Package{ + Package: &proto.Package{ + Name: "", // empty package name + }, + }, + } + + ctx := &ZRpcContext{ + NameFromFilename: false, + } + + // Should fall back to filename when package name is empty + serviceName := determineServiceName(p, ctx) + assert.Equal(t, "myservice", serviceName) +}