feat: add cmdline argument to control whether generate package name from proto filename (#5387)

This commit is contained in:
Kevin Wan
2026-01-24 19:47:14 +08:00
committed by GitHub
parent 6e1af75635
commit 173f76acf9
9 changed files with 221 additions and 24 deletions

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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,

View File

@@ -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))
}

View File

@@ -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)
}