diff --git a/tools/goctl/internal/version/version.go b/tools/goctl/internal/version/version.go index 4a5eec4c5..07c07eef9 100644 --- a/tools/goctl/internal/version/version.go +++ b/tools/goctl/internal/version/version.go @@ -6,7 +6,7 @@ import ( ) // BuildVersion is the version of goctl. -const BuildVersion = "1.10.0" +const BuildVersion = "1.11.0" var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-beta": 2, "beta": 3, "released": 4, "": 5} diff --git a/tools/goctl/rpc/CHANGELOG-cn.md b/tools/goctl/rpc/CHANGELOG-cn.md new file mode 100644 index 000000000..eb4229efa --- /dev/null +++ b/tools/goctl/rpc/CHANGELOG-cn.md @@ -0,0 +1,117 @@ +# 变更日志 + +## 未发布 + +### 新功能 + +#### 外部 Proto 导入支持(`--proto_path` / `-I`) + +新增通过 `-I` / `--proto_path` 标志导入外部目录中的 proto 文件,支持完整的传递性依赖解析。 + +**涉及文件:** +- `generator/gen.go` — `ZRpcContext` 新增 `ProtoPaths` 字段;新增 `resolveImportedProtos()` 在代码生成前填充 `ImportedProtos`。 +- `generator/genpb.go` — 新增 `buildProtocCmd()` 自动发现并追加传递性导入的 proto 文件到 `protoc` 命令;新增 `relativeToProtoPath()` 计算正确的相对路径。 +- `parser/import.go` — 新文件(主要新增)。实现 `ResolveImports()` 递归解析传递性导入,`ParseImportedProtos()` 提取导入 proto 的 `go_package` / `package` 元数据,`BuildProtoPackageMap()` 构建按 proto 包名的 O(1) 查找表。 +- `parser/proto.go` — `Proto` 结构体新增 `ImportedProtos []ImportedProto` 字段。 +- `cli/cli.go` — `RPCNew` 传递 `ProtoPaths` 到 `ZRpcContext`。 +- `cli/zrpc.go` — 将 `VarStringSliceProtoPath` 传递到 `ZRpcContext.ProtoPaths`。 + +**前后对比:** + +| | 变更前 | 变更后 | +|---|---|---| +| 从外部目录导入 Proto | ❌ 不支持,所有类型必须在同一文件中定义 | ✅ 使用 `-I ./ext_protos` 添加搜索路径 | +| 传递性导入(A → B → C) | ❌ 仅识别直接导入 | ✅ 递归解析所有传递性依赖 | +| 导入 proto 的 `.pb.go` 生成 | ❌ 需手动为每个文件单独运行 protoc | ✅ 自动将导入的 proto 追加到 protoc 命令 | +| Proto 搜索路径 | ❌ 仅源文件所在目录 | ✅ 支持多个 `-I` 路径,与 protoc 一致 | + +**行为说明:** +- 递归遍历 proto 文件中的所有 `import` 声明,跳过 `google/*` 知名类型。 +- 在每个 `-I` 目录中搜索被导入的文件,未找到的系统级 proto 静默跳过。 +- 将发现的 proto 文件追加到 `protoc` 命令,使其 `.pb.go` 文件与主 proto 一同生成。 + +--- + +#### 跨包类型解析 + +当导入的 proto 与主 proto 具有**不同**的 `go_package` 时,goctl 现在能够自动在 server、logic 和 client 代码中生成正确的 Go 导入路径和限定类型引用。 + +**涉及文件:** +- `generator/typeref.go` — 新文件,核心类型解析引擎: + - `resolveRPCTypeRef()` — 将 proto RPC 类型(简单类型、同包点号类型、跨包点号类型、Google WKT)解析为带正确导入路径的 Go 类型引用。 + - `resolveCallTypeRef()` — 客户端代码生成变体,支持类型别名。 + - `googleWKTTable` — 全部 16 种 Google 知名类型到 Go 等价类型的映射表。 +- `generator/genserver.go` — `genFunctions()` 调用 `resolveRPCTypeRef()` 解析请求/响应类型并收集额外导入路径。 +- `generator/genlogic.go` — `genLogicFunction()` 使用 `resolveRPCTypeRef()`;新增 `addLogicImports()` 按需添加主 pb 导入和跨包导入。 +- `generator/gencall.go` — `genFunction()` 和 `getInterfaceFuncs()` 使用 `resolveCallTypeRef()` 处理类型别名和额外导入;新增 `buildExtraImportLines()` 辅助函数。 +- `generator/call.tpl` — 新增 `{{.extraImports}}` 占位符用于跨包导入行。 + +**前后对比:** + +| Proto 类型 | 变更前 | 变更后 | +|---|---|---| +| `GetReq`(同文件) | `pb.GetReq` | `pb.GetReq`(无变化) | +| `ext.ExtReq`(相同 `go_package`) | ❌ 报错:"request type must defined in" | ✅ `pb.ExtReq` — 合并到主包 | +| `common.TypesReq`(不同 `go_package`) | ❌ 报错:"request type must defined in" | ✅ `common.TypesReq` + 自动生成 `import "example.com/demo/pb/common"` | +| `google.protobuf.Empty` | ❌ 报错:"request type must defined in" | ✅ `emptypb.Empty` + 自动生成导入 | + +**行为说明:** +- 简单类型(如 `GetReq`)解析为 `pb.GetReq`,无额外导入。 +- 同包点号类型(如 `ext.ExtReq`,其中 `ext` 与主 proto 有相同的 `go_package`)解析为 `pb.ExtReq`。 +- 跨包点号类型(如 `common.TypesReq`,其中 `common` 有不同的 `go_package`)解析为 `common.TypesReq`,并自动添加正确的 Go 导入路径。 + +--- + +#### Google 知名类型作为 RPC 参数 + +Google protobuf 知名类型现在可以直接用作 RPC 的请求/响应类型(而不仅仅是消息字段)。 + +**涉及文件:** +- `generator/typeref.go` — `resolveGoogleWKT()` + `googleWKTTable` 处理所有标准类型。 + +**前后对比:** + +| Proto 类型 | 变更前(作为 RPC 参数) | 变更后(作为 RPC 参数) | +|---|---|---| +| `google.protobuf.Empty` | ❌ 报错 | ✅ `emptypb.Empty` | +| `google.protobuf.Timestamp` | ❌ 报错 | ✅ `timestamppb.Timestamp` | +| `google.protobuf.Duration` | ❌ 报错 | ✅ `durationpb.Duration` | +| `google.protobuf.Any` | ❌ 报错 | ✅ `anypb.Any` | +| `google.protobuf.Struct` | ❌ 报错 | ✅ `structpb.Struct` | +| `google.protobuf.FieldMask` | ❌ 报错 | ✅ `fieldmaskpb.FieldMask` | +| `google.protobuf.*Value` | ❌ 报错 | ✅ `wrapperspb.*Value` | + +> 注:这些类型此前已可用作**消息字段**。本次变更使其可直接用作 **RPC 请求/响应类型**。 + +**完整类型映射表:** + +| Proto 类型 | Go 类型 | +|---|---| +| `google.protobuf.Empty` | `emptypb.Empty` | +| `google.protobuf.Timestamp` | `timestamppb.Timestamp` | +| `google.protobuf.Duration` | `durationpb.Duration` | +| `google.protobuf.Any` | `anypb.Any` | +| `google.protobuf.Struct` | `structpb.Struct` | +| `google.protobuf.Value` | `structpb.Value` | +| `google.protobuf.ListValue` | `structpb.ListValue` | +| `google.protobuf.FieldMask` | `fieldmaskpb.FieldMask` | +| `google.protobuf.*Value`(包装类型) | `wrapperspb.*Value` | + +--- + +### 不兼容变更 + +#### RPC 定义中允许使用点号类型名 + +此前 goctl 会拒绝 RPC 请求/响应类型中包含点号的情况(如 `base.Req`),要求所有类型必须定义在同一个 proto 文件中。此限制已移除。 + +**前后对比:** + +| Proto 定义 | 变更前 | 变更后 | +|---|---|---| +| `rpc Fetch(base.Req) returns (base.Reply)` | ❌ 解析错误:"request type must defined in xxx.proto" | ✅ 解析成功,`base.Req` 通过导入的 proto 解析 | +| `rpc Ping(google.protobuf.Empty) returns (Reply)` | ❌ 解析错误:"request type must defined in xxx.proto" | ✅ 解析成功,解析为 `emptypb.Empty` | + +**涉及文件:** +- `parser/service.go` — 移除了拒绝点号类型名的验证循环(原错误信息为 `"request type must defined in"` / `"returns type must defined in"`)。 +- `parser/parser_test.go` — `TestDefaultProtoParseCaseInvalidRequestType` 和 `TestDefaultProtoParseCaseInvalidResponseType` 重命名并更新,验证点号类型现在可以正常解析。 diff --git a/tools/goctl/rpc/CHANGELOG.md b/tools/goctl/rpc/CHANGELOG.md new file mode 100644 index 000000000..dd4ddd379 --- /dev/null +++ b/tools/goctl/rpc/CHANGELOG.md @@ -0,0 +1,117 @@ +# Changelog + +## Unreleased + +### New Features + +#### External Proto Import Support (`--proto_path` / `-I`) + +Added support for importing proto files from external directories via `-I` / `--proto_path` flags, with full transitive dependency resolution. + +**Affected files:** +- `generator/gen.go` — Added `ProtoPaths` field to `ZRpcContext`; added `resolveImportedProtos()` to populate `ImportedProtos` before code generation. +- `generator/genpb.go` — Added `buildProtocCmd()` to automatically discover and append transitively imported proto files to the `protoc` command; added `relativeToProtoPath()` to compute correct relative paths for protoc output. +- `parser/import.go` — New file (major addition). Implements `ResolveImports()` for recursive transitive import resolution, `ParseImportedProtos()` for extracting `go_package` / `package` metadata from imported protos, and `BuildProtoPackageMap()` for O(1) lookup by proto package name. +- `parser/proto.go` — Added `ImportedProtos []ImportedProto` field to the `Proto` struct. +- `cli/cli.go` — Passes `ProtoPaths` from `RPCNew` to `ZRpcContext`. +- `cli/zrpc.go` — Passes `VarStringSliceProtoPath` to `ZRpcContext.ProtoPaths`. + +**Before vs After:** + +| | Before | After | +|---|---|---| +| Proto imports from external dirs | ❌ Not supported, all types must be in the same file | ✅ Use `-I ./ext_protos` to add search paths | +| Transitive imports (A → B → C) | ❌ Only direct imports recognized | ✅ Recursively resolves all transitive dependencies | +| Imported proto `.pb.go` generation | ❌ Manual, must run protoc separately for each file | ✅ Automatic, imported protos appended to protoc command | +| Proto search paths | ❌ Only source file directory | ✅ Multiple `-I` paths, same as protoc | + +**Behavior:** +- Transitively walks all `import` declarations in proto files, skipping `google/*` well-known types. +- Searches each `-I` directory for imported files, silently skipping system-level protos not found in user paths. +- Appends discovered proto files to the `protoc` command so their `.pb.go` files are generated alongside the main proto. + +--- + +#### Cross-Package Type Resolution + +When an imported proto has a **different** `go_package` from the main proto, goctl now automatically generates the correct Go import paths and qualified type references in server, logic, and client code. + +**Affected files:** +- `generator/typeref.go` — New file. Core type resolution engine: + - `resolveRPCTypeRef()` — Resolves proto RPC types (simple, same-package dotted, cross-package dotted, Google WKT) to Go type references with correct import paths. + - `resolveCallTypeRef()` — Variant for client code generation with type alias support. + - `googleWKTTable` — Mapping table for all 16 Google well-known types to their Go equivalents. +- `generator/genserver.go` — `genFunctions()` now calls `resolveRPCTypeRef()` for request/response types and collects extra import paths. +- `generator/genlogic.go` — `genLogicFunction()` uses `resolveRPCTypeRef()`; added `addLogicImports()` to conditionally include main pb import and cross-package imports. +- `generator/gencall.go` — `genFunction()` and `getInterfaceFuncs()` use `resolveCallTypeRef()` for type aliases and extra imports; added `buildExtraImportLines()` helper. +- `generator/call.tpl` — Added `{{.extraImports}}` placeholder for cross-package import lines. + +**Before vs After:** + +| Proto type | Before | After | +|---|---|---| +| `GetReq` (same file) | `pb.GetReq` | `pb.GetReq` (unchanged) | +| `ext.ExtReq` (same `go_package`) | ❌ Error: "request type must defined in" | ✅ `pb.ExtReq` — merged into main package | +| `common.TypesReq` (different `go_package`) | ❌ Error: "request type must defined in" | ✅ `common.TypesReq` + auto-generated `import "example.com/demo/pb/common"` | +| `google.protobuf.Empty` | ❌ Error: "request type must defined in" | ✅ `emptypb.Empty` + auto-generated import | + +**Behavior:** +- Simple types (e.g., `GetReq`) resolve to `pb.GetReq` with no extra import. +- Same-package dotted types (e.g., `ext.ExtReq` where `ext` has the same `go_package`) resolve to `pb.ExtReq`. +- Cross-package dotted types (e.g., `common.TypesReq` where `common` has a different `go_package`) resolve to `common.TypesReq` with the correct Go import path added automatically. + +--- + +#### Google Well-Known Types as RPC Parameters + +Google protobuf well-known types can now be used directly as RPC request/response types (not just as message fields). + +**Affected files:** +- `generator/typeref.go` — `resolveGoogleWKT()` + `googleWKTTable` handles all standard types. + +**Before vs After:** + +| Proto Type | Before (as RPC param) | After (as RPC param) | +|---|---|---| +| `google.protobuf.Empty` | ❌ Error | ✅ `emptypb.Empty` | +| `google.protobuf.Timestamp` | ❌ Error | ✅ `timestamppb.Timestamp` | +| `google.protobuf.Duration` | ❌ Error | ✅ `durationpb.Duration` | +| `google.protobuf.Any` | ❌ Error | ✅ `anypb.Any` | +| `google.protobuf.Struct` | ❌ Error | ✅ `structpb.Struct` | +| `google.protobuf.FieldMask` | ❌ Error | ✅ `fieldmaskpb.FieldMask` | +| `google.protobuf.*Value` | ❌ Error | ✅ `wrapperspb.*Value` | + +> Note: These types were already usable as **message fields** before. This change allows them as **RPC request/response types** directly. + +**Supported types:** + +| Proto Type | Go Type | +|---|---| +| `google.protobuf.Empty` | `emptypb.Empty` | +| `google.protobuf.Timestamp` | `timestamppb.Timestamp` | +| `google.protobuf.Duration` | `durationpb.Duration` | +| `google.protobuf.Any` | `anypb.Any` | +| `google.protobuf.Struct` | `structpb.Struct` | +| `google.protobuf.Value` | `structpb.Value` | +| `google.protobuf.ListValue` | `structpb.ListValue` | +| `google.protobuf.FieldMask` | `fieldmaskpb.FieldMask` | +| `google.protobuf.*Value` (wrappers) | `wrapperspb.*Value` | + +--- + +### Breaking Changes + +#### Dotted Type Names Now Allowed in RPC Definitions + +Previously, goctl rejected any RPC request/response type containing a dot (e.g., `base.Req`), requiring all types to be defined in the same proto file. This restriction has been removed. + +**Before vs After:** + +| Proto Definition | Before | After | +|---|---|---| +| `rpc Fetch(base.Req) returns (base.Reply)` | ❌ Parse error: "request type must defined in xxx.proto" | ✅ Parsed successfully, `base.Req` resolved via imported proto | +| `rpc Ping(google.protobuf.Empty) returns (Reply)` | ❌ Parse error: "request type must defined in xxx.proto" | ✅ Parsed successfully, resolved to `emptypb.Empty` | + +**Affected files:** +- `parser/service.go` — Removed the validation loop that rejected dotted type names with `"request type must defined in"` / `"returns type must defined in"` errors. +- `parser/parser_test.go` — `TestDefaultProtoParseCaseInvalidRequestType` and `TestDefaultProtoParseCaseInvalidResponseType` renamed and updated to verify that dotted types now parse successfully. diff --git a/tools/goctl/rpc/README-cn.md b/tools/goctl/rpc/README-cn.md new file mode 100644 index 000000000..cf534ec08 --- /dev/null +++ b/tools/goctl/rpc/README-cn.md @@ -0,0 +1,315 @@ +# goctl rpc — RPC 代码生成 + +[English](README.md) | 中文 + +goctl rpc 是 `goctl` 脚手架下的 RPC 服务代码生成模块,基于 `.proto` 文件生成完整的 zRPC 服务代码。你只需编写 proto 定义和业务逻辑,其余代码均由工具自动生成。 + +## 特性 + +- **贴近 protoc**:与 protoc 完全兼容,透传所有 protoc 参数 +- **外部 Proto 导入**:支持跨目录、跨包的 proto 导入,自动解析传递性依赖 +- **多服务模式**:单个 proto 文件中定义多个 service,按服务名自动分组 +- **流式支持**:支持服务端流、客户端流和双向流 +- **Google 标准类型**:自动识别 `google.protobuf.*` 类型并生成正确的 Go 导入 +- **客户端生成**:自动生成封装好的 RPC 客户端代码 + +## 前置条件 + +```bash +# 安装 protoc 插件 +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +``` + +## 快速开始 + +### 方式一:一键创建服务 + +```bash +goctl rpc new greeter +``` + +生成完整的项目结构: + +``` +greeter/ +├── etc/ +│ └── greeter.yaml +├── greeter/ +│ ├── greeter.pb.go +│ └── greeter_grpc.pb.go +├── greeter.go +├── greeter.proto +├── greeterclient/ +│ └── greeter.go +└── internal/ + ├── config/ + │ └── config.go + ├── logic/ + │ └── pinglogic.go + ├── server/ + │ └── greeterserver.go + └── svc/ + └── servicecontext.go +``` + +### 方式二:基于 Proto 文件生成 + +1. 生成 proto 模板: + +```bash +goctl rpc template -o=user.proto +``` + +2. 初始化输出目录并生成服务代码: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +goctl rpc protoc user.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . +``` + +--- + +## 命令参考 + +### `goctl rpc protoc` + +从 `.proto` 文件生成 zRPC 服务代码。 + +```bash +goctl rpc protoc [flags] +``` + +**示例:** + +```bash +# 基础用法 +goctl rpc protoc user.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . + +# 多服务模式 +goctl rpc protoc multi.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . -m + +# 导入外部 proto +goctl rpc protoc service.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . -I ./shared_protos + +# 使用 Google 标准类型 +goctl rpc protoc service.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . +``` + +**参数说明:** + +| 参数 | 缩写 | 类型 | 默认值 | 说明 | +|------|------|------|--------|------| +| `--zrpc_out` | | string | **必填** | zRPC 服务代码输出目录 | +| `--go_out` | | string | **必填** | protoc Go 代码输出目录 | +| `--go-grpc_out` | | string | **必填** | protoc gRPC 代码输出目录 | +| `--go_opt` | | string | | protoc-gen-go 选项(如 `module=example.com/demo`) | +| `--go-grpc_opt` | | string | | protoc-gen-go-grpc 选项(如 `module=example.com/demo`) | +| `--proto_path` | `-I` | string[] | | proto 导入搜索目录(可多次指定) | +| `--multiple` | `-m` | bool | `false` | 多服务模式 | +| `--client` | `-c` | bool | `true` | 是否生成 RPC 客户端代码 | +| `--style` | | string | `gozero` | 文件命名风格 | +| `--module` | | string | | 自定义 Go module 名称 | +| `--name-from-filename` | | bool | `false` | 使用文件名而非 package 名命名服务 | +| `--verbose` | `-v` | bool | `false` | 显示详细日志 | +| `--home` | | string | | goctl 模板目录 | +| `--remote` | | string | | 远程模板 Git 仓库地址 | +| `--branch` | | string | | 远程模板分支 | + +### `goctl rpc new` + +快速创建一个完整的 RPC 服务项目。 + +```bash +goctl rpc new [flags] +``` + +**参数说明:** + +| 参数 | 缩写 | 类型 | 默认值 | 说明 | +|------|------|------|--------|------| +| `--style` | | string | `gozero` | 文件命名风格 | +| `--client` | `-c` | bool | `true` | 是否生成 RPC 客户端代码 | +| `--module` | | string | | 自定义 Go module 名称 | +| `--verbose` | `-v` | bool | `false` | 显示详细日志 | +| `--idea` | | bool | `false` | 生成 IDE 项目标记 | +| `--name-from-filename` | | bool | `false` | 使用文件名而非 package 名命名服务 | +| `--home` | | string | | goctl 模板目录 | +| `--remote` | | string | | 远程模板 Git 仓库地址 | +| `--branch` | | string | | 远程模板分支 | + +### `goctl rpc template` + +生成 proto 文件模板。 + +```bash +goctl rpc template -o= [flags] +``` + +**参数说明:** + +| 参数 | 类型 | 说明 | +|------|------|------| +| `-o` | string | 输出文件路径(必填) | +| `--home` | string | goctl 模板目录 | +| `--remote` | string | 远程模板 Git 仓库地址 | +| `--branch` | string | 远程模板分支 | + +--- + +## 功能详解 + +### 多服务模式(`--multiple`) + +当 proto 文件包含多个 `service` 定义时,必须使用 `--multiple` 标志。 + +```protobuf +service SearchService { + rpc Search(SearchReq) returns (SearchReply); +} + +service NotifyService { + rpc Notify(NotifyReq) returns (NotifyReply); +} +``` + +**启用 `--multiple` 后的目录变化:** + +| 特性 | 默认模式 | `--multiple` 模式 | +|------|---------|-------------------| +| 服务数量 | 仅 1 个 | 1 个或多个 | +| 客户端目录 | 以服务名命名 | 固定为 `client/` | +| 代码组织 | 扁平结构 | 按服务名分组 | + +**`--multiple=false`(默认)的目录结构:** + +``` +output/ +├── greeterclient/ +│ └── greeter.go +├── internal/ +│ ├── logic/ +│ │ └── sayhellologic.go +│ └── server/ +│ └── greeterserver.go +└── ... +``` + +**`--multiple=true` 的目录结构:** + +``` +output/ +├── client/ +│ ├── searchservice/ +│ │ └── searchservice.go +│ └── notifyservice/ +│ └── notifyservice.go +├── internal/ +│ ├── logic/ +│ │ ├── searchservice/ +│ │ │ └── searchlogic.go +│ │ └── notifyservice/ +│ │ └── notifylogic.go +│ └── server/ +│ ├── searchservice/ +│ │ └── searchserviceserver.go +│ └── notifyservice/ +│ └── notifyserviceserver.go +└── ... +``` + +### 外部 Proto 导入(`--proto_path`) + +通过 `-I` / `--proto_path` 指定额外的 proto 搜索目录,支持以下场景: + +- **同目录导入**:`import "types.proto";` +- **子目录导入**:`import "common/types.proto";` +- **外部目录导入**:proto 文件位于项目外部 +- **传递性导入**:A 导入 B,B 导入 C,goctl 自动递归解析 +- **跨包导入**:不同 `go_package` 的 proto 文件,自动生成正确的 Go 导入 + +```bash +# 从多个目录搜索 proto 文件 +goctl rpc protoc service.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . -I ./shared_protos -I /path/to/external_protos +``` + +### 服务命名 + +默认情况下,服务名称来自 proto 的 **package 名称**(例如 `package user;` → 服务名 `user`)。这使得多个 proto 文件可以共享同一个 package: + +``` +protos/ +├── user_base.proto # package user; +├── user_auth.proto # package user; +└── user_profile.proto # package user; +``` + +三个文件会生成到同一个 `user` 服务中。 + +如需使用 proto 文件名命名(旧版行为),请添加 `--name-from-filename` 标志。 + +### 流式 RPC + +支持 gRPC 的三种流式模式: + +```protobuf +service StreamService { + rpc ServerStream(Req) returns (stream Reply); // 服务端流 + rpc ClientStream(stream Req) returns (Reply); // 客户端流 + rpc BidiStream(stream Req) returns (stream Reply); // 双向流 +} +``` + +### Google 标准类型 + +goctl 自动识别并正确处理 Google protobuf 标准类型: + +| Proto 类型 | Go 类型 | +|-----------|---------| +| `google.protobuf.Empty` | `emptypb.Empty` | +| `google.protobuf.Timestamp` | `timestamppb.Timestamp` | +| `google.protobuf.Duration` | `durationpb.Duration` | +| `google.protobuf.Any` | `anypb.Any` | +| `google.protobuf.Struct` | `structpb.Struct` | +| `google.protobuf.FieldMask` | `fieldmaskpb.FieldMask` | +| `google.protobuf.*Value` | `wrapperspb.*Value` | + +这些类型可直接用作 RPC 参数类型,goctl 会自动生成正确的导入。 + +--- + +## 完整示例 + +详见 [example/](example/) 目录,包含 10 个完整示例,覆盖所有生成场景。 + +| # | 示例 | 场景 | +|---|------|------| +| 01 | [基础服务](example/01-basic/) | 单服务,无导入 | +| 02 | [同级导入](example/02-import-sibling/) | 导入同目录 proto | +| 03 | [子目录导入](example/03-import-subdir/) | 导入子目录 proto | +| 04 | [传递性导入](example/04-transitive-import/) | A → B → C 依赖链 | +| 05 | [多服务](example/05-multiple-services/) | `--multiple` 模式 | +| 06 | [标准类型](example/06-wellknown-types/) | 消息中使用 Timestamp 等 | +| 07 | [外部 Proto(同包)](example/07-external-proto-same-pkg/) | 外部 proto,相同 go_package | +| 08 | [外部 Proto(跨包)](example/08-external-proto-diff-pkg/) | 外部 proto,不同 go_package | +| 09 | [标准类型作参数](example/09-google-types-as-rpc/) | Empty/Timestamp 作为 RPC 参数 | +| 10 | [流式通信](example/10-streaming/) | 服务端/客户端/双向流 | diff --git a/tools/goctl/rpc/README.md b/tools/goctl/rpc/README.md index 2073255f6..4a1f655e9 100644 --- a/tools/goctl/rpc/README.md +++ b/tools/goctl/rpc/README.md @@ -1,228 +1,315 @@ -# Rpc Generation +# goctl rpc — RPC Code Generation -Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。 +English | [中文](README-cn.md) -## 特性 +goctl rpc is the RPC service code generation module of the `goctl` scaffold. It generates a complete zRPC service from `.proto` files. You only need to write the proto definition and business logic — all boilerplate code is generated automatically. -* 简单易用 -* 快速提升开发效率 -* 出错率低 -* 贴近 protoc +## Features +- **protoc compatible**: Fully compatible with protoc, all protoc arguments are passed through +- **External proto imports**: Cross-directory and cross-package proto imports with automatic transitive dependency resolution +- **Multiple services**: Define multiple services in a single proto file, auto-grouped by service name +- **Streaming support**: Server streaming, client streaming, and bidirectional streaming +- **Google well-known types**: Automatic recognition of `google.protobuf.*` types with correct Go imports +- **Client generation**: Auto-generated RPC client wrapper code -## 快速开始 - -### 方式一:快速生成greet服务 - - 通过命令 `goctl rpc new ${servieName}`生成 - - 如生成greet rpc服务: - - ```Bash - goctl rpc new greet - ``` - - 执行后代码结构如下: - -```text -. -└── greet - ├── etc - │   └── greet.yaml - ├── greet - │   ├── greet.go - │   ├── greet.pb.go - │   └── greet_grpc.pb.go - ├── greet.go - ├── greet.proto - └── internal - ├── config - │   └── config.go - ├── logic - │   └── pinglogic.go - ├── server - │   └── greetserver.go - └── svc - └── servicecontext.go -``` - -### 方式二:通过指定proto生成rpc服务 - -* 生成proto模板 - -```Bash -$ goctl rpc template -o=user.proto -``` - -```proto -syntax = "proto3"; - -package user; -option go_package="./user"; - -message Request { - string ping = 1; -} - -message Response { - string pong = 1; -} - -service User { - rpc Ping(Request) returns(Response); -} -``` - - -* 生成rpc服务代码 +## Prerequisites ```bash -$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. +# Install protoc plugins +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest ``` +## Quick Start -## 用法 +### Method 1: Create a Service Instantly -### rpc 服务生成用法 - -```Bash -$ goctl rpc protoc -h -Generate grpc code - -Usage: - goctl rpc protoc [flags] - -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 - --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 +```bash +goctl rpc new greeter ``` -### 参数说明 +Generates a complete project structure: -* --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输出目录 +``` +greeter/ +├── etc/ +│ └── greeter.yaml +├── greeter/ +│ ├── greeter.pb.go +│ └── greeter_grpc.pb.go +├── greeter.go +├── greeter.proto +├── greeterclient/ +│ └── greeter.go +└── internal/ + ├── config/ + │ └── config.go + ├── logic/ + │ └── pinglogic.go + ├── server/ + │ └── greeterserver.go + └── svc/ + └── servicecontext.go +``` -> ## --multiple -> 是否开启多个 rpc service 生成,如果开启,则满足一下新特性 -> 1. 支持 1 到多个 rpc service -> 2. 生成 rpc 服务会按照服务名称分组(尽管只有一个 rpc service) -> 3. rpc client 的文件目录变更为固定名称 `client` -> -> 如果不开启,则和旧版本 rpc 生成逻辑一样(兼容) -> 1. 有且只能有一个 rpc service +### Method 2: Generate from a Proto File -> ## 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 -> ``` +1. Generate a proto template: +```bash +goctl rpc template -o=user.proto +``` -## rpc 服务生成 example -详情见 [example/rpc](https://github.com/zeromicro/go-zero/tree/master/tools/goctl/example) +2. Initialize the output directory and generate service code: -## --multiple 为 true 和 false 的目录区别 -源 proto 文件 +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +goctl rpc protoc user.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . +``` + +--- + +## Command Reference + +### `goctl rpc protoc` + +Generate zRPC service code from a `.proto` file. + +```bash +goctl rpc protoc [flags] +``` + +**Examples:** + +```bash +# Basic usage +goctl rpc protoc user.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . + +# Multiple services mode +goctl rpc protoc multi.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . -m + +# Import external protos +goctl rpc protoc service.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . -I ./shared_protos + +# Use Google well-known types +goctl rpc protoc service.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . +``` + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--zrpc_out` | | string | **required** | Output directory for zRPC service code | +| `--go_out` | | string | **required** | Output directory for protoc Go code | +| `--go-grpc_out` | | string | **required** | Output directory for protoc gRPC code | +| `--go_opt` | | string | | Options for protoc-gen-go (e.g., `module=example.com/demo`) | +| `--go-grpc_opt` | | string | | Options for protoc-gen-go-grpc (e.g., `module=example.com/demo`) | +| `--proto_path` | `-I` | string[] | | Proto import search directories (repeatable) | +| `--multiple` | `-m` | bool | `false` | Multiple services mode | +| `--client` | `-c` | bool | `true` | Generate RPC client code | +| `--style` | | string | `gozero` | File naming style | +| `--module` | | string | | Custom Go module name | +| `--name-from-filename` | | bool | `false` | Use filename instead of package name for service naming | +| `--verbose` | `-v` | bool | `false` | Enable verbose logging | +| `--home` | | string | | goctl template directory | +| `--remote` | | string | | Remote template Git repository URL | +| `--branch` | | string | | Remote template branch | + +### `goctl rpc new` + +Quickly create a complete RPC service project. + +```bash +goctl rpc new [flags] +``` + +**Flags:** + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--style` | | string | `gozero` | File naming style | +| `--client` | `-c` | bool | `true` | Generate RPC client code | +| `--module` | | string | | Custom Go module name | +| `--verbose` | `-v` | bool | `false` | Enable verbose logging | +| `--idea` | | bool | `false` | Generate IDE project marker | +| `--name-from-filename` | | bool | `false` | Use filename instead of package name for service naming | +| `--home` | | string | | goctl template directory | +| `--remote` | | string | | Remote template Git repository URL | +| `--branch` | | string | | Remote template branch | + +### `goctl rpc template` + +Generate a proto file template. + +```bash +goctl rpc template -o= [flags] +``` + +**Flags:** + +| Flag | Type | Description | +|------|------|-------------| +| `-o` | string | Output file path (required) | +| `--home` | string | goctl template directory | +| `--remote` | string | Remote template Git repository URL | +| `--branch` | string | Remote template branch | + +--- + +## Feature Details + +### Multiple Services Mode (`--multiple`) + +When a proto file contains multiple `service` definitions, the `--multiple` flag is required. ```protobuf -syntax = "proto3"; - -package hello; - -option go_package = "./hello"; - -message HelloReq { - string in = 1; +service SearchService { + rpc Search(SearchReq) returns (SearchReply); } -message HelloResp { - string msg = 1; -} - -service Greet { - rpc SayHello(HelloReq) returns (HelloResp); +service NotifyService { + rpc Notify(NotifyReq) returns (NotifyReply); } ``` -### --multiple=true +**Directory differences with `--multiple`:** -```text -hello -├── client // 区别1:rpc client 目录固定为 client 名称 -│   └── greet // 区别2:会按照 rpc service 名称分组 -│   └── greet.go -├── etc -│   └── hello.yaml -├── hello.go -├── internal -│   ├── config -│   │   └── config.go -│   ├── logic -│   │   └── greet // 区别2:会按照 rpc service 名称分组 -│   │   └── sayhellologic.go -│   ├── server -│   │   └── greet // 区别2:会按照 rpc service 名称分组 -│   │   └── greetserver.go -│   └── svc -│   └── servicecontext.go -└── pb - └── hello - ├── hello.pb.go - └── hello_grpc.pb.go +| Feature | Default mode | `--multiple` mode | +|---------|-------------|-------------------| +| Services per proto | Exactly 1 | 1 or more | +| Client directory | Named after service | Fixed `client/` directory | +| Code organization | Flat structure | Grouped by service name | + +**`--multiple=false` (default) directory structure:** + +``` +output/ +├── greeterclient/ +│ └── greeter.go +├── internal/ +│ ├── logic/ +│ │ └── sayhellologic.go +│ └── server/ +│ └── greeterserver.go +└── ... ``` -### --multiple=false (旧版本目录,向后兼容) -```text -hello -├── etc -│   └── hello.yaml -├── greet -│   └── greet.go -├── hello.go -├── internal -│   ├── config -│   │   └── config.go -│   ├── logic -│   │   └── sayhellologic.go -│   ├── server -│   │   └── greetserver.go -│   └── svc -│   └── servicecontext.go -└── pb - └── hello - ├── hello.pb.go - └── hello_grpc.pb.go -``` \ No newline at end of file +**`--multiple=true` directory structure:** + +``` +output/ +├── client/ +│ ├── searchservice/ +│ │ └── searchservice.go +│ └── notifyservice/ +│ └── notifyservice.go +├── internal/ +│ ├── logic/ +│ │ ├── searchservice/ +│ │ │ └── searchlogic.go +│ │ └── notifyservice/ +│ │ └── notifylogic.go +│ └── server/ +│ ├── searchservice/ +│ │ └── searchserviceserver.go +│ └── notifyservice/ +│ └── notifyserviceserver.go +└── ... +``` + +### External Proto Imports (`--proto_path`) + +Use `-I` / `--proto_path` to specify additional proto search directories. Supported scenarios: + +- **Same-directory import**: `import "types.proto";` +- **Subdirectory import**: `import "common/types.proto";` +- **External directory import**: Proto files outside the project +- **Transitive imports**: A imports B, B imports C — goctl resolves recursively +- **Cross-package imports**: Different `go_package` values generate correct Go imports automatically + +```bash +# Search multiple directories for proto files +goctl rpc protoc service.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . -I ./shared_protos -I /path/to/external_protos +``` + +### Service Naming + +By default, the service name is derived from the proto **package name** (e.g., `package user;` → service name `user`). This allows multiple proto files to share the same package: + +``` +protos/ +├── user_base.proto # package user; +├── user_auth.proto # package user; +└── user_profile.proto # package user; +``` + +All three files generate into a single `user` service. + +To use the proto filename for naming (legacy behavior), add the `--name-from-filename` flag. + +### Streaming RPC + +All three gRPC streaming patterns are supported: + +```protobuf +service StreamService { + rpc ServerStream(Req) returns (stream Reply); // Server streaming + rpc ClientStream(stream Req) returns (Reply); // Client streaming + rpc BidiStream(stream Req) returns (stream Reply); // Bidirectional streaming +} +``` + +### Google Well-Known Types + +goctl automatically recognizes and handles Google protobuf well-known types: + +| Proto Type | Go Type | +|-----------|---------| +| `google.protobuf.Empty` | `emptypb.Empty` | +| `google.protobuf.Timestamp` | `timestamppb.Timestamp` | +| `google.protobuf.Duration` | `durationpb.Duration` | +| `google.protobuf.Any` | `anypb.Any` | +| `google.protobuf.Struct` | `structpb.Struct` | +| `google.protobuf.FieldMask` | `fieldmaskpb.FieldMask` | +| `google.protobuf.*Value` | `wrapperspb.*Value` | + +These types can be used directly as RPC parameter types — goctl generates the correct imports automatically. + +--- + +## Examples + +See the [example/](example/) directory for 10 complete examples covering all generation scenarios. + +| # | Example | Scenario | +|---|---------|----------| +| 01 | [Basic service](example/01-basic/) | Single service, no imports | +| 02 | [Sibling import](example/02-import-sibling/) | Import from same directory | +| 03 | [Subdirectory import](example/03-import-subdir/) | Import from subdirectory | +| 04 | [Transitive import](example/04-transitive-import/) | A → B → C dependency chain | +| 05 | [Multiple services](example/05-multiple-services/) | `--multiple` mode | +| 06 | [Well-known types](example/06-wellknown-types/) | Timestamp etc. in messages | +| 07 | [External proto (same pkg)](example/07-external-proto-same-pkg/) | External proto, same go_package | +| 08 | [External proto (diff pkg)](example/08-external-proto-diff-pkg/) | External proto, different go_package | +| 09 | [Google types as params](example/09-google-types-as-rpc/) | Empty/Timestamp as RPC parameters | +| 10 | [Streaming](example/10-streaming/) | Server/client/bidirectional streaming | diff --git a/tools/goctl/rpc/cli/cli.go b/tools/goctl/rpc/cli/cli.go index 81f146a8d..9d2e23210 100644 --- a/tools/goctl/rpc/cli/cli.go +++ b/tools/goctl/rpc/cli/cli.go @@ -98,6 +98,7 @@ func RPCNew(_ *cobra.Command, args []string) error { ctx.IsGenClient = VarBoolClient ctx.Module = VarStringModule ctx.NameFromFilename = VarBoolNameFromFilename + ctx.ProtoPaths = []string{filepath.Dir(src)} grpcOptList := VarStringSliceGoGRPCOpt if len(grpcOptList) > 0 { diff --git a/tools/goctl/rpc/cli/zrpc.go b/tools/goctl/rpc/cli/zrpc.go index 41f906ff7..9a458e036 100644 --- a/tools/goctl/rpc/cli/zrpc.go +++ b/tools/goctl/rpc/cli/zrpc.go @@ -105,6 +105,7 @@ func ZRPC(_ *cobra.Command, args []string) error { ctx.IsGenClient = VarBoolClient ctx.Module = VarStringModule ctx.NameFromFilename = VarBoolNameFromFilename + ctx.ProtoPaths = VarStringSliceProtoPath g := generator.NewGenerator(style, verbose) return g.Generate(&ctx) } diff --git a/tools/goctl/rpc/example/01-basic/README-cn.md b/tools/goctl/rpc/example/01-basic/README-cn.md new file mode 100644 index 000000000..6de478ef3 --- /dev/null +++ b/tools/goctl/rpc/example/01-basic/README-cn.md @@ -0,0 +1,100 @@ +# 示例 01:基础 RPC 服务 + +这是使用 goctl 生成 RPC 服务的最简单示例。 + +## Proto 定义 + +一个 `greeter.proto` 文件,包含一个服务和一个 RPC 方法,无外部导入。 + +`go_package` 使用完整的模块路径: + +```protobuf +option go_package = "example.com/demo/greeter"; +``` + +## 生成命令 + +### 方式一:使用 `goctl rpc new` 快速创建 + +```bash +# 一条命令创建完整的 RPC 项目 +goctl rpc new greeter +``` + +该命令会同时生成 proto 文件和服务代码: + +``` +greeter/ +├── etc +│ └── greeter.yaml +├── greeter +│ ├── greeter.pb.go +│ └── greeter_grpc.pb.go +├── greeter.go +├── greeter.proto +├── greeterclient +│ └── greeter.go +└── internal + ├── config + │ └── config.go + ├── logic + │ └── pinglogic.go + ├── server + │ └── greeterserver.go + └── svc + └── servicecontext.go +``` + +### 方式二:基于已有 Proto 文件生成 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc greeter.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── greeter.yaml +├── go.mod +├── greeter +│ ├── greeter.pb.go +│ └── greeter_grpc.pb.go +├── greeter.go +├── greeterclient +│ └── greeter.go +└── internal + ├── config + │ └── config.go + ├── logic + │ └── sayhellologic.go + ├── server + │ └── greeterserver.go + └── svc + └── servicecontext.go +``` + +## 要点说明 + +- 这是最简单的场景:一个 proto 文件、一个服务、一个 RPC 方法。 +- `go_package` 使用完整的模块路径(`example.com/demo/greeter`),而非相对路径。 +- `--module` 告诉 goctl Go 模块名;`--go_opt=module=...` 和 `--go-grpc_opt=module=...` 告诉 protoc 从输出路径中去除模块前缀。 +- `--zrpc_out` 指定 goctl 生成的服务代码输出目录。 +- `--go_out` 和 `--go-grpc_out` 指定 protoc 生成代码的输出目录。 +- 编辑逻辑文件(`internal/logic/sayhellologic.go`)来实现业务逻辑。 diff --git a/tools/goctl/rpc/example/01-basic/README.md b/tools/goctl/rpc/example/01-basic/README.md new file mode 100644 index 000000000..a0067f897 --- /dev/null +++ b/tools/goctl/rpc/example/01-basic/README.md @@ -0,0 +1,100 @@ +# Example 01: Basic RPC Service + +This is the simplest example of generating an RPC service with goctl. + +## Proto Definition + +A single `greeter.proto` file with one service and one RPC method, no external imports. + +The `go_package` uses a full module path: + +```protobuf +option go_package = "example.com/demo/greeter"; +``` + +## Generation Commands + +### Method 1: Quick Start with `goctl rpc new` + +```bash +# Create a complete RPC project with one command +goctl rpc new greeter +``` + +This generates the proto file and service code together: + +``` +greeter/ +├── etc +│ └── greeter.yaml +├── greeter +│ ├── greeter.pb.go +│ └── greeter_grpc.pb.go +├── greeter.go +├── greeter.proto +├── greeterclient +│ └── greeter.go +└── internal + ├── config + │ └── config.go + ├── logic + │ └── pinglogic.go + ├── server + │ └── greeterserver.go + └── svc + └── servicecontext.go +``` + +### Method 2: Generate from an Existing Proto + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc greeter.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── greeter.yaml +├── go.mod +├── greeter +│ ├── greeter.pb.go +│ └── greeter_grpc.pb.go +├── greeter.go +├── greeterclient +│ └── greeter.go +└── internal + ├── config + │ └── config.go + ├── logic + │ └── sayhellologic.go + ├── server + │ └── greeterserver.go + └── svc + └── servicecontext.go +``` + +## Key Points + +- This is the simplest scenario: one proto file, one service, one RPC method. +- The `go_package` uses a full module path (`example.com/demo/greeter`), not a relative path. +- The `--module` flag tells goctl the Go module name; `--go_opt=module=...` and `--go-grpc_opt=module=...` tell protoc to strip the module prefix from output paths. +- The `--zrpc_out` flag specifies where the goctl-generated service code goes. +- The `--go_out` and `--go-grpc_out` flags specify where protoc-generated code goes. +- Edit the logic file (`internal/logic/sayhellologic.go`) to implement your business logic. diff --git a/tools/goctl/rpc/example/01-basic/greeter.proto b/tools/goctl/rpc/example/01-basic/greeter.proto new file mode 100644 index 000000000..3f927cd08 --- /dev/null +++ b/tools/goctl/rpc/example/01-basic/greeter.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package greeter; + +option go_package = "example.com/demo/greeter"; + +message HelloReq { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +service Greeter { + rpc SayHello(HelloReq) returns (HelloReply); +} diff --git a/tools/goctl/rpc/example/02-import-sibling/README-cn.md b/tools/goctl/rpc/example/02-import-sibling/README-cn.md new file mode 100644 index 000000000..c08369d2c --- /dev/null +++ b/tools/goctl/rpc/example/02-import-sibling/README-cn.md @@ -0,0 +1,77 @@ +# 示例 02:导入同级 Proto 文件 + +本示例演示如何导入同一目录下的 proto 文件。 + +## Proto 定义 + +同一目录下的两个 proto 文件共享相同的 `go_package`: + +- `types.proto` — 定义共享消息类型(`User`)。 +- `user.proto` — 定义 RPC 服务,导入 `types.proto`。 + +两个文件使用相同的 `go_package`,采用完整模块路径: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +`user.proto` 通过以下方式导入 `types.proto`: + +```protobuf +import "types.proto"; +``` + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc user.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── usersvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── createuserlogic.go +│ │ └── getuserlogic.go +│ ├── server +│ │ └── userserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── types.pb.go +│ ├── user.pb.go +│ └── user_grpc.pb.go +├── userservice +│ └── userservice.go +└── usersvc.go +``` + +## 要点说明 + +- 两个 proto 文件(`user.proto` 和 `types.proto`)共享相同的 `go_package = "example.com/demo/pb"`,编译到同一个 Go 包中。 +- `user.proto` 通过 `import "types.proto"` 导入 `types.proto`。 +- 多个 proto 文件共享相同的 `go_package` 时,它们会编译到同一个 Go 包中。 +- 只需将包含 `service` 定义的 proto 文件传递给 `goctl rpc protoc`。 +- 导入的 proto 文件会被 protoc 自动编译,并由 goctl 自动解析。 diff --git a/tools/goctl/rpc/example/02-import-sibling/README.md b/tools/goctl/rpc/example/02-import-sibling/README.md new file mode 100644 index 000000000..743dca8c8 --- /dev/null +++ b/tools/goctl/rpc/example/02-import-sibling/README.md @@ -0,0 +1,77 @@ +# Example 02: Importing a Sibling Proto File + +This example demonstrates importing a proto file from the same directory. + +## Proto Definition + +Two proto files in the same directory share the same `go_package`: + +- `types.proto` — Defines shared message types (`User`). +- `user.proto` — Defines the RPC service, importing `types.proto`. + +Both files use the same `go_package` with a full module path: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +`user.proto` imports `types.proto` via: + +```protobuf +import "types.proto"; +``` + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc user.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── usersvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── createuserlogic.go +│ │ └── getuserlogic.go +│ ├── server +│ │ └── userserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── types.pb.go +│ ├── user.pb.go +│ └── user_grpc.pb.go +├── userservice +│ └── userservice.go +└── usersvc.go +``` + +## Key Points + +- Two proto files (`user.proto` and `types.proto`) share the same `go_package = "example.com/demo/pb"`, compiled into a single Go package. +- `user.proto` imports `types.proto` via `import "types.proto"`. +- When multiple proto files share the same `go_package`, they compile into a single Go package. +- Only the proto file containing `service` definitions needs to be passed to `goctl rpc protoc`. +- The imported proto is automatically compiled by protoc and resolved by goctl. diff --git a/tools/goctl/rpc/example/02-import-sibling/types.proto b/tools/goctl/rpc/example/02-import-sibling/types.proto new file mode 100644 index 000000000..f86996523 --- /dev/null +++ b/tools/goctl/rpc/example/02-import-sibling/types.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package types; + +option go_package = "example.com/demo/pb"; + +message User { + string id = 1; + string name = 2; + int32 age = 3; +} diff --git a/tools/goctl/rpc/example/02-import-sibling/user.proto b/tools/goctl/rpc/example/02-import-sibling/user.proto new file mode 100644 index 000000000..f12daf3e7 --- /dev/null +++ b/tools/goctl/rpc/example/02-import-sibling/user.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package usersvc; + +option go_package = "example.com/demo/pb"; + +import "types.proto"; + +message GetUserReq { + string id = 1; +} + +message GetUserReply { + types.User user = 1; +} + +message CreateUserReq { + string name = 1; + int32 age = 2; +} + +message CreateUserReply { + types.User user = 1; +} + +service UserService { + rpc GetUser(GetUserReq) returns (GetUserReply); + rpc CreateUser(CreateUserReq) returns (CreateUserReply); +} diff --git a/tools/goctl/rpc/example/03-import-subdir/README-cn.md b/tools/goctl/rpc/example/03-import-subdir/README-cn.md new file mode 100644 index 000000000..8190873c0 --- /dev/null +++ b/tools/goctl/rpc/example/03-import-subdir/README-cn.md @@ -0,0 +1,82 @@ +# 示例 03:导入子目录中的 Proto 文件 + +本示例演示如何导入子目录中的 proto 文件。 + +## Proto 定义 + +两个 proto 文件有**不同**的 `go_package` 值: + +- `order.proto` — 定义 `OrderService`,导入 `common/types.proto`。 + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +- `common/types.proto` — 定义可复用的分页和排序消息。 + +```protobuf +option go_package = "example.com/demo/pb/common"; +``` + +`order.proto` 从子目录导入 `common/types.proto`: + +```protobuf +import "common/types.proto"; +``` + +注意两个文件的 `go_package` **不同**,因此会编译到不同的 Go 包中。 + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc order.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── ordersvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── getorderlogic.go +│ │ └── listorderslogic.go +│ ├── server +│ │ └── orderserviceserver.go +│ └── svc +│ └── servicecontext.go +├── orderservice +│ └── orderservice.go +├── ordersvc.go +└── pb + ├── common + │ └── types.pb.go + ├── order.pb.go + └── order_grpc.pb.go +``` + +## 要点说明 + +- 两个 proto 文件有**不同**的 `go_package` 值,编译到不同的 Go 包中(`pb/` 和 `pb/common/`)。 +- `order.proto` 从子目录导入 `common/types.proto`。 +- 当导入的 proto 文件有不同的 `go_package` 时,goctl 会自动生成跨包导入。 +- `-I .` 告诉 protoc 从当前目录开始搜索,使其能够找到 `common/types.proto`。 diff --git a/tools/goctl/rpc/example/03-import-subdir/README.md b/tools/goctl/rpc/example/03-import-subdir/README.md new file mode 100644 index 000000000..038cb3b1e --- /dev/null +++ b/tools/goctl/rpc/example/03-import-subdir/README.md @@ -0,0 +1,82 @@ +# Example 03: Importing Proto from a Subdirectory + +This example demonstrates importing a proto file from a subdirectory. + +## Proto Definition + +Two proto files with **different** `go_package` values: + +- `order.proto` — Defines the `OrderService`, imports `common/types.proto`. + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +- `common/types.proto` — Defines reusable pagination and sorting messages. + +```protobuf +option go_package = "example.com/demo/pb/common"; +``` + +`order.proto` imports `common/types.proto` from a subdirectory: + +```protobuf +import "common/types.proto"; +``` + +Note that the two files have **different** `go_package` values, so they compile into separate Go packages. + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc order.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── ordersvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── getorderlogic.go +│ │ └── listorderslogic.go +│ ├── server +│ │ └── orderserviceserver.go +│ └── svc +│ └── servicecontext.go +├── orderservice +│ └── orderservice.go +├── ordersvc.go +└── pb + ├── common + │ └── types.pb.go + ├── order.pb.go + └── order_grpc.pb.go +``` + +## Key Points + +- Two proto files have **different** `go_package` values, so they compile into separate Go packages (`pb/` and `pb/common/`). +- `order.proto` imports `common/types.proto` from a subdirectory. +- When imported protos have a different `go_package`, goctl automatically generates cross-package imports. +- The `-I .` flag tells protoc to search from the current directory, enabling it to find `common/types.proto`. diff --git a/tools/goctl/rpc/example/03-import-subdir/common/types.proto b/tools/goctl/rpc/example/03-import-subdir/common/types.proto new file mode 100644 index 000000000..2e6df891f --- /dev/null +++ b/tools/goctl/rpc/example/03-import-subdir/common/types.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package common; + +option go_package = "example.com/demo/pb/common"; + +message PageInfo { + int32 page = 1; + int32 size = 2; +} + +message SortInfo { + string field = 1; + string order = 2; +} diff --git a/tools/goctl/rpc/example/03-import-subdir/order.proto b/tools/goctl/rpc/example/03-import-subdir/order.proto new file mode 100644 index 000000000..35bfcc105 --- /dev/null +++ b/tools/goctl/rpc/example/03-import-subdir/order.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package ordersvc; + +option go_package = "example.com/demo/pb"; + +import "common/types.proto"; + +message OrderItem { + string id = 1; + string name = 2; + double price = 3; +} + +message ListOrdersReq { + common.PageInfo page = 1; + common.SortInfo sort = 2; +} + +message ListOrdersReply { + repeated OrderItem orders = 1; +} + +message GetOrderReq { + string id = 1; +} + +message GetOrderReply { + OrderItem order = 1; +} + +service OrderService { + rpc ListOrders(ListOrdersReq) returns (ListOrdersReply); + rpc GetOrder(GetOrderReq) returns (GetOrderReply); +} diff --git a/tools/goctl/rpc/example/04-transitive-import/README-cn.md b/tools/goctl/rpc/example/04-transitive-import/README-cn.md new file mode 100644 index 000000000..8f7c1b429 --- /dev/null +++ b/tools/goctl/rpc/example/04-transitive-import/README-cn.md @@ -0,0 +1,72 @@ +# 示例 04:传递性导入 + +本示例演示 proto 的传递性导入,即 A 导入 B,B 导入 C。 + +## Proto 定义 + +三个 proto 文件形成传递导入链,共享相同的 `go_package`: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +- `base.proto` — C 层:定义基础类型(`BaseResp`)。 +- `middleware.proto` — B 层:导入 `base.proto`,定义 `RequestMeta`。 +- `main.proto` — A 层:导入 `middleware.proto`,定义 `PingService`(入口文件)。 + +导入链:`main.proto` → `middleware.proto` → `base.proto` + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc main.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── pingsvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ └── pinglogic.go +│ ├── server +│ │ └── pingserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── base.pb.go +│ ├── main.pb.go +│ ├── main_grpc.pb.go +│ └── middleware.pb.go +├── pingservice +│ └── pingservice.go +└── pingsvc.go +``` + +## 要点说明 + +- 三个 proto 文件(`base.proto` → `middleware.proto` → `main.proto`)形成传递导入链。 +- goctl 自动递归解析所有传递导入。 +- 三个文件共享相同的 `go_package = "example.com/demo/pb"`。 +- 只需指定入口 proto 文件,goctl 和 protoc 会自动处理其余部分。 +- 循环导入会被检测并报错(与 protoc 行为一致)。 diff --git a/tools/goctl/rpc/example/04-transitive-import/README.md b/tools/goctl/rpc/example/04-transitive-import/README.md new file mode 100644 index 000000000..8f88d2381 --- /dev/null +++ b/tools/goctl/rpc/example/04-transitive-import/README.md @@ -0,0 +1,72 @@ +# Example 04: Transitive Imports + +This example demonstrates transitive proto imports, where A imports B and B imports C. + +## Proto Definition + +Three proto files form a transitive import chain, all sharing the same `go_package`: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +- `base.proto` — Layer C: defines base types (`BaseResp`). +- `middleware.proto` — Layer B: imports `base.proto`, defines `RequestMeta`. +- `main.proto` — Layer A: imports `middleware.proto`, defines the `PingService` (entry point). + +Import chain: `main.proto` → `middleware.proto` → `base.proto` + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc main.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── pingsvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ └── pinglogic.go +│ ├── server +│ │ └── pingserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── base.pb.go +│ ├── main.pb.go +│ ├── main_grpc.pb.go +│ └── middleware.pb.go +├── pingservice +│ └── pingservice.go +└── pingsvc.go +``` + +## Key Points + +- Three proto files (`base.proto` → `middleware.proto` → `main.proto`) form a transitive import chain. +- goctl recursively resolves all transitive imports automatically. +- All three files share the same `go_package = "example.com/demo/pb"`. +- You only need to specify the entry proto file — goctl and protoc handle the rest. +- Circular imports are detected and will cause an error (same as protoc behavior). diff --git a/tools/goctl/rpc/example/04-transitive-import/base.proto b/tools/goctl/rpc/example/04-transitive-import/base.proto new file mode 100644 index 000000000..c5c93a932 --- /dev/null +++ b/tools/goctl/rpc/example/04-transitive-import/base.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package base; + +option go_package = "example.com/demo/pb"; + +message BaseResp { + int32 code = 1; + string msg = 2; +} diff --git a/tools/goctl/rpc/example/04-transitive-import/main.proto b/tools/goctl/rpc/example/04-transitive-import/main.proto new file mode 100644 index 000000000..ad00f5dd8 --- /dev/null +++ b/tools/goctl/rpc/example/04-transitive-import/main.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package pingsvc; + +option go_package = "example.com/demo/pb"; + +import "middleware.proto"; + +message PingReq { + middleware.RequestMeta meta = 1; +} + +message PingReply { + string pong = 1; +} + +service PingService { + rpc Ping(PingReq) returns (PingReply); +} diff --git a/tools/goctl/rpc/example/04-transitive-import/middleware.proto b/tools/goctl/rpc/example/04-transitive-import/middleware.proto new file mode 100644 index 000000000..a6dba17e4 --- /dev/null +++ b/tools/goctl/rpc/example/04-transitive-import/middleware.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package middleware; + +option go_package = "example.com/demo/pb"; + +import "base.proto"; + +message RequestMeta { + string trace_id = 1; + base.BaseResp base = 2; +} diff --git a/tools/goctl/rpc/example/05-multiple-services/README-cn.md b/tools/goctl/rpc/example/05-multiple-services/README-cn.md new file mode 100644 index 000000000..2c9c7791c --- /dev/null +++ b/tools/goctl/rpc/example/05-multiple-services/README-cn.md @@ -0,0 +1,80 @@ +# 示例 05:多服务模式(`--multiple`) + +本示例演示从一个 proto 文件生成多个 RPC 服务。 + +## Proto 定义 + +两个 proto 文件共享相同的 `go_package`: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +- `shared.proto` — 定义共享消息类型(`Meta`)。 +- `multi.proto` — 定义了**两个**服务:`SearchService` 和 `NotifyService`。 + +当 proto 文件包含多个 `service` 块时,必须使用 `-m`(或 `--multiple`)标志。 + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后使用 `-m` 标志生成代码: + +```bash +goctl rpc protoc multi.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . \ + -m +``` + +生成的目录结构: + +``` +output/ +├── client +│ ├── notifyservice +│ │ └── notifyservice.go +│ └── searchservice +│ └── searchservice.go +├── etc +│ └── multisvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── notifyservice +│ │ │ └── notifylogic.go +│ │ └── searchservice +│ │ └── searchlogic.go +│ ├── server +│ │ ├── notifyservice +│ │ │ └── notifyserviceserver.go +│ │ └── searchservice +│ │ └── searchserviceserver.go +│ └── svc +│ └── servicecontext.go +├── multisvc.go +└── pb + ├── multi.pb.go + ├── multi_grpc.pb.go + └── shared.pb.go +``` + +## 要点说明 + +- `-m`(或 `--multiple`)标志启用多服务模式。 +- 多服务模式下,`client/` 包含按服务名分组的子目录;`logic/` 和 `server/` 也按服务名分组。 +- 两个服务共享一个入口文件(`multisvc.go`)和配置。 +- 不使用 `--multiple` 时,goctl 只允许每个 proto 文件有一个 `service` 块。 +- 所有服务共享同一个 `config.go` 和 `servicecontext.go`。 diff --git a/tools/goctl/rpc/example/05-multiple-services/README.md b/tools/goctl/rpc/example/05-multiple-services/README.md new file mode 100644 index 000000000..13b610700 --- /dev/null +++ b/tools/goctl/rpc/example/05-multiple-services/README.md @@ -0,0 +1,80 @@ +# Example 05: Multiple Services (`--multiple`) + +This example demonstrates generating multiple RPC services from a single proto file. + +## Proto Definition + +Two proto files share the same `go_package`: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +- `shared.proto` — Defines shared message types (`Meta`). +- `multi.proto` — Defines **two** services: `SearchService` and `NotifyService`. + +The `-m` (or `--multiple`) flag is required when a proto file contains more than one `service` block. + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code with the `-m` flag: + +```bash +goctl rpc protoc multi.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . \ + -m +``` + +Generated directory structure: + +``` +output/ +├── client +│ ├── notifyservice +│ │ └── notifyservice.go +│ └── searchservice +│ └── searchservice.go +├── etc +│ └── multisvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── notifyservice +│ │ │ └── notifylogic.go +│ │ └── searchservice +│ │ └── searchlogic.go +│ ├── server +│ │ ├── notifyservice +│ │ │ └── notifyserviceserver.go +│ │ └── searchservice +│ │ └── searchserviceserver.go +│ └── svc +│ └── servicecontext.go +├── multisvc.go +└── pb + ├── multi.pb.go + ├── multi_grpc.pb.go + └── shared.pb.go +``` + +## Key Points + +- The `-m` (or `--multiple`) flag enables multiple-service mode. +- In multiple mode, `client/` contains per-service subdirectories; `logic/` and `server/` are also grouped by service name. +- Both services share a single entry point (`multisvc.go`) and config. +- Without `--multiple`, goctl only allows one `service` block per proto file. +- All services share the same `config.go` and `servicecontext.go`. diff --git a/tools/goctl/rpc/example/05-multiple-services/multi.proto b/tools/goctl/rpc/example/05-multiple-services/multi.proto new file mode 100644 index 000000000..6bd0ee006 --- /dev/null +++ b/tools/goctl/rpc/example/05-multiple-services/multi.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package multisvc; + +option go_package = "example.com/demo/pb"; + +import "shared.proto"; + +message SearchReq { + shared.Meta meta = 1; + string keyword = 2; +} + +message SearchReply { + repeated string items = 1; +} + +message NotifyReq { + shared.Meta meta = 1; + string message = 2; +} + +message NotifyReply { + bool ok = 1; +} + +service SearchService { + rpc Search(SearchReq) returns (SearchReply); +} + +service NotifyService { + rpc Notify(NotifyReq) returns (NotifyReply); +} diff --git a/tools/goctl/rpc/example/05-multiple-services/shared.proto b/tools/goctl/rpc/example/05-multiple-services/shared.proto new file mode 100644 index 000000000..b310e20f7 --- /dev/null +++ b/tools/goctl/rpc/example/05-multiple-services/shared.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package shared; + +option go_package = "example.com/demo/pb"; + +message Meta { + string trace_id = 1; + string version = 2; +} diff --git a/tools/goctl/rpc/example/06-wellknown-types/README-cn.md b/tools/goctl/rpc/example/06-wellknown-types/README-cn.md new file mode 100644 index 000000000..e3d75b25c --- /dev/null +++ b/tools/goctl/rpc/example/06-wellknown-types/README-cn.md @@ -0,0 +1,65 @@ +# 示例 06:知名类型 + +本示例演示如何使用 Google protobuf 知名类型(`Timestamp`、`Duration`、`Any`)作为消息字段。 + +## Proto 定义 + +`events.proto` 使用 `google.protobuf.Timestamp` 作为消息字段类型。 + +`go_package` 使用完整的模块路径: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc events.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── eventsvc.yaml +├── eventservice +│ └── eventservice.go +├── eventsvc.go +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── createeventlogic.go +│ │ └── listeventslogic.go +│ ├── server +│ │ └── eventserviceserver.go +│ └── svc +│ └── servicecontext.go +└── pb + ├── events.pb.go + └── events_grpc.pb.go +``` + +## 要点说明 + +- 使用 Google 知名类型(`google.protobuf.Timestamp`、`google.protobuf.Duration`、`google.protobuf.Any`)作为消息字段。 +- goctl 自动将知名类型映射到 Go 导入包(`timestamppb`、`durationpb`、`anypb` 等)。 +- 如果 protoc 已正确安装,知名类型无需额外的 `--proto_path`。 diff --git a/tools/goctl/rpc/example/06-wellknown-types/README.md b/tools/goctl/rpc/example/06-wellknown-types/README.md new file mode 100644 index 000000000..5a4591d2a --- /dev/null +++ b/tools/goctl/rpc/example/06-wellknown-types/README.md @@ -0,0 +1,65 @@ +# Example 06: Well-Known Types + +This example demonstrates using Google protobuf well-known types (`Timestamp`, `Duration`, `Any`) as message fields. + +## Proto Definition + +`events.proto` uses `google.protobuf.Timestamp` as a message field type. + +The `go_package` uses a full module path: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc events.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── eventsvc.yaml +├── eventservice +│ └── eventservice.go +├── eventsvc.go +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── createeventlogic.go +│ │ └── listeventslogic.go +│ ├── server +│ │ └── eventserviceserver.go +│ └── svc +│ └── servicecontext.go +└── pb + ├── events.pb.go + └── events_grpc.pb.go +``` + +## Key Points + +- Uses Google well-known types (`google.protobuf.Timestamp`, `google.protobuf.Duration`, `google.protobuf.Any`) as message fields. +- goctl automatically maps well-known types to Go imports (`timestamppb`, `durationpb`, `anypb`, etc.). +- No extra `--proto_path` needed for well-known types if protoc is properly installed. diff --git a/tools/goctl/rpc/example/06-wellknown-types/events.proto b/tools/goctl/rpc/example/06-wellknown-types/events.proto new file mode 100644 index 000000000..41955104d --- /dev/null +++ b/tools/goctl/rpc/example/06-wellknown-types/events.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package eventsvc; + +option go_package = "example.com/demo/pb"; + +import "google/protobuf/timestamp.proto"; + +message Event { + string id = 1; + string name = 2; + google.protobuf.Timestamp created_at = 3; +} + +message CreateEventReq { + string name = 1; +} + +message CreateEventReply { + Event event = 1; +} + +message ListEventsReq { + int32 page = 1; + int32 size = 2; +} + +message ListEventsReply { + repeated Event events = 1; +} + +service EventService { + rpc CreateEvent(CreateEventReq) returns (CreateEventReply); + rpc ListEvents(ListEventsReq) returns (ListEventsReply); +} diff --git a/tools/goctl/rpc/example/07-external-proto-same-pkg/README-cn.md b/tools/goctl/rpc/example/07-external-proto-same-pkg/README-cn.md new file mode 100644 index 000000000..af7f360fc --- /dev/null +++ b/tools/goctl/rpc/example/07-external-proto-same-pkg/README-cn.md @@ -0,0 +1,77 @@ +# 示例 07:外部 Proto — 相同 `go_package` + +本示例演示从外部目录导入 proto 文件,且两个文件共享**相同**的 `go_package`。 + +## Proto 定义 + +`service.proto` 和 `ext.proto` 使用相同的 `go_package`: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +源码布局: + +``` +07-external-proto-same-pkg/ +├── ext_protos +│ └── ext.proto # 外部 proto(go_package = "example.com/demo/pb") +├── service.proto # 服务定义(go_package = "example.com/demo/pb") +├── README.md +└── README-cn.md +``` + +- `ext.proto` 位于独立目录(`ext_protos/`),但与 `service.proto` 有相同的 `go_package`。 +- `service.proto` 导入 `ext.proto`,使用 `ext.ExtReq` / `ext.ExtReply` 作为 RPC 类型。 + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码(注意 `-I ./ext_protos`): + +```bash +goctl rpc protoc service.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . -I ./ext_protos +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── svc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ └── querylogic.go +│ ├── server +│ │ └── queryserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── ext.pb.go +│ ├── service.pb.go +│ └── service_grpc.pb.go +├── queryservice +│ └── queryservice.go +└── svc.go +``` + +## 要点说明 + +- `ext.proto` 位于独立目录(`ext_protos/`),但与 `service.proto` 有相同的 `go_package`。 +- 使用 `-I ./ext_protos` 将外部目录添加到 proto 搜索路径。 +- 当外部 proto 有**相同**的 `go_package` 时,所有类型合并到一个 Go 包中——无需跨包导入。 diff --git a/tools/goctl/rpc/example/07-external-proto-same-pkg/README.md b/tools/goctl/rpc/example/07-external-proto-same-pkg/README.md new file mode 100644 index 000000000..83fa396d6 --- /dev/null +++ b/tools/goctl/rpc/example/07-external-proto-same-pkg/README.md @@ -0,0 +1,77 @@ +# Example 07: External Proto — Same `go_package` + +This example demonstrates importing proto files from an external directory where both files share the **same** `go_package`. + +## Proto Definition + +Both `service.proto` and `ext.proto` use the same `go_package`: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +Source layout: + +``` +07-external-proto-same-pkg/ +├── ext_protos +│ └── ext.proto # External proto (go_package = "example.com/demo/pb") +├── service.proto # Service definition (go_package = "example.com/demo/pb") +├── README.md +└── README-cn.md +``` + +- `ext.proto` lives in a separate directory (`ext_protos/`), but has the same `go_package` as `service.proto`. +- `service.proto` imports `ext.proto` and uses `ext.ExtReq` / `ext.ExtReply` as RPC types. + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code (note `-I ./ext_protos`): + +```bash +goctl rpc protoc service.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . -I ./ext_protos +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── svc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ └── querylogic.go +│ ├── server +│ │ └── queryserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── ext.pb.go +│ ├── service.pb.go +│ └── service_grpc.pb.go +├── queryservice +│ └── queryservice.go +└── svc.go +``` + +## Key Points + +- `ext.proto` lives in a separate directory (`ext_protos/`), but has the same `go_package` as `service.proto`. +- Use `-I ./ext_protos` to add the external directory to the proto search path. +- When the external proto has the **same** `go_package`, all types merge into one Go package — no cross-package imports are needed. diff --git a/tools/goctl/rpc/example/07-external-proto-same-pkg/ext_protos/ext.proto b/tools/goctl/rpc/example/07-external-proto-same-pkg/ext_protos/ext.proto new file mode 100644 index 000000000..4c9d3abc7 --- /dev/null +++ b/tools/goctl/rpc/example/07-external-proto-same-pkg/ext_protos/ext.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package ext; + +option go_package = "example.com/demo/pb"; + +message ExtReq { + string key = 1; +} + +message ExtReply { + string value = 1; + int32 code = 2; +} diff --git a/tools/goctl/rpc/example/07-external-proto-same-pkg/service.proto b/tools/goctl/rpc/example/07-external-proto-same-pkg/service.proto new file mode 100644 index 000000000..3b8667988 --- /dev/null +++ b/tools/goctl/rpc/example/07-external-proto-same-pkg/service.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package svc; + +option go_package = "example.com/demo/pb"; + +import "ext.proto"; + +service QueryService { + rpc Query(ext.ExtReq) returns (ext.ExtReply); +} diff --git a/tools/goctl/rpc/example/08-external-proto-diff-pkg/README-cn.md b/tools/goctl/rpc/example/08-external-proto-diff-pkg/README-cn.md new file mode 100644 index 000000000..5c04a464f --- /dev/null +++ b/tools/goctl/rpc/example/08-external-proto-diff-pkg/README-cn.md @@ -0,0 +1,78 @@ +# 示例 08:外部 Proto — 不同 `go_package` + +本示例演示从外部目录导入 proto 文件,且文件具有**不同**的 `go_package` 值,需要在生成的 Go 代码中进行跨包导入。 + +## Proto 定义 + +proto 文件使用不同的 `go_package` 值: + +- `service.proto`:`go_package = "example.com/demo/pb"` +- `ext_protos/common/types.proto`:`go_package = "example.com/demo/pb/common"` + +源码布局: + +``` +08-external-proto-diff-pkg/ +├── ext_protos +│ └── common +│ └── types.proto # 外部 proto(go_package = "example.com/demo/pb/common") +├── service.proto # 服务定义(go_package = "example.com/demo/pb") +├── README.md +└── README-cn.md +``` + +- `types.proto` 的 `go_package = "example.com/demo/pb/common"` — **不同**的 Go 包。 +- `service.proto` 直接使用 `common.ExtReq` / `common.ExtReply` 作为 RPC 参数类型。 + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码(注意 `-I ./ext_protos`): + +```bash +goctl rpc protoc service.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . -I ./ext_protos +``` + +生成的目录结构: + +``` +output/ +├── dataservice +│ └── dataservice.go +├── etc +│ └── svc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ └── fetchlogic.go +│ ├── server +│ │ └── dataserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── common +│ │ └── types.pb.go +│ ├── service.pb.go +│ └── service_grpc.pb.go +└── svc.go +``` + +## 要点说明 + +- 当外部 proto 有**不同**的 `go_package` 时,goctl 会自动生成跨包 Go 导入。 +- goctl 通过解析导入 proto 的 `go_package` 选项,将 proto 包名(如 `common`)映射到正确的 Go 导入路径。 +- `service.proto` 直接使用 `common.ExtReq` / `common.ExtReply` 作为 RPC 参数类型。 diff --git a/tools/goctl/rpc/example/08-external-proto-diff-pkg/README.md b/tools/goctl/rpc/example/08-external-proto-diff-pkg/README.md new file mode 100644 index 000000000..5ff239596 --- /dev/null +++ b/tools/goctl/rpc/example/08-external-proto-diff-pkg/README.md @@ -0,0 +1,78 @@ +# Example 08: External Proto — Different `go_package` + +This example demonstrates importing proto files from an external directory where the files have **different** `go_package` values, requiring cross-package imports in the generated Go code. + +## Proto Definition + +The proto files use different `go_package` values: + +- `service.proto`: `go_package = "example.com/demo/pb"` +- `ext_protos/common/types.proto`: `go_package = "example.com/demo/pb/common"` + +Source layout: + +``` +08-external-proto-diff-pkg/ +├── ext_protos +│ └── common +│ └── types.proto # External proto (go_package = "example.com/demo/pb/common") +├── service.proto # Service definition (go_package = "example.com/demo/pb") +├── README.md +└── README-cn.md +``` + +- `types.proto` has `go_package = "example.com/demo/pb/common"` — a **different** Go package. +- `service.proto` uses `common.ExtReq` / `common.ExtReply` directly as RPC parameter types. + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code (note `-I ./ext_protos`): + +```bash +goctl rpc protoc service.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . -I ./ext_protos +``` + +Generated directory structure: + +``` +output/ +├── dataservice +│ └── dataservice.go +├── etc +│ └── svc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ └── fetchlogic.go +│ ├── server +│ │ └── dataserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── common +│ │ └── types.pb.go +│ ├── service.pb.go +│ └── service_grpc.pb.go +└── svc.go +``` + +## Key Points + +- When the external proto has a **different** `go_package`, goctl generates cross-package Go imports automatically. +- goctl resolves the proto package name (e.g., `common`) to the correct Go import path by parsing the imported proto's `go_package` option. +- `service.proto` uses `common.ExtReq` / `common.ExtReply` directly as RPC parameter types. diff --git a/tools/goctl/rpc/example/08-external-proto-diff-pkg/ext_protos/common/types.proto b/tools/goctl/rpc/example/08-external-proto-diff-pkg/ext_protos/common/types.proto new file mode 100644 index 000000000..f96efa8d1 --- /dev/null +++ b/tools/goctl/rpc/example/08-external-proto-diff-pkg/ext_protos/common/types.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package common; + +option go_package = "example.com/demo/pb/common"; + +message ExtReq { + string key = 1; + string source = 2; +} + +message ExtReply { + string value = 1; + int32 code = 2; +} diff --git a/tools/goctl/rpc/example/08-external-proto-diff-pkg/service.proto b/tools/goctl/rpc/example/08-external-proto-diff-pkg/service.proto new file mode 100644 index 000000000..f5ac3eaeb --- /dev/null +++ b/tools/goctl/rpc/example/08-external-proto-diff-pkg/service.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package svc; + +option go_package = "example.com/demo/pb"; + +import "common/types.proto"; + +service DataService { + rpc Fetch(common.ExtReq) returns (common.ExtReply); +} diff --git a/tools/goctl/rpc/example/09-google-types-as-rpc/README-cn.md b/tools/goctl/rpc/example/09-google-types-as-rpc/README-cn.md new file mode 100644 index 000000000..40ae635af --- /dev/null +++ b/tools/goctl/rpc/example/09-google-types-as-rpc/README-cn.md @@ -0,0 +1,65 @@ +# 示例 09:Google 类型作为 RPC 参数 + +本示例演示将 Google protobuf 知名类型**直接**用作 RPC 请求或响应类型(而不仅仅是消息字段)。 + +## Proto 定义 + +`service.proto` 使用 `google.protobuf.Empty` 和 `google.protobuf.Timestamp` 直接作为 RPC 请求/响应类型。 + +`go_package` 使用完整的模块路径: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc service.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── healthsvc.yaml +├── go.mod +├── healthservice +│ └── healthservice.go +├── healthsvc.go +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── gettimelogic.go +│ │ └── pinglogic.go +│ ├── server +│ │ └── healthserviceserver.go +│ └── svc +│ └── servicecontext.go +└── pb + ├── service.pb.go + └── service_grpc.pb.go +``` + +## 要点说明 + +- 使用 Google 知名类型(`google.protobuf.Empty`、`google.protobuf.Timestamp`)直接作为 RPC 请求/响应类型(不仅仅是消息字段)。 +- goctl 正确将其映射到 Go 类型(`emptypb.Empty`、`timestamppb.Timestamp`)并生成正确的导入。 +- 这与示例 06 不同,示例 06 中知名类型用作消息字段。 diff --git a/tools/goctl/rpc/example/09-google-types-as-rpc/README.md b/tools/goctl/rpc/example/09-google-types-as-rpc/README.md new file mode 100644 index 000000000..08b55d82d --- /dev/null +++ b/tools/goctl/rpc/example/09-google-types-as-rpc/README.md @@ -0,0 +1,65 @@ +# Example 09: Google Types as RPC Parameters + +This example demonstrates using Google protobuf well-known types **directly** as RPC request or response types (not just as message fields). + +## Proto Definition + +`service.proto` uses `google.protobuf.Empty` and `google.protobuf.Timestamp` directly as RPC request/response types. + +The `go_package` uses a full module path: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc service.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── healthsvc.yaml +├── go.mod +├── healthservice +│ └── healthservice.go +├── healthsvc.go +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── gettimelogic.go +│ │ └── pinglogic.go +│ ├── server +│ │ └── healthserviceserver.go +│ └── svc +│ └── servicecontext.go +└── pb + ├── service.pb.go + └── service_grpc.pb.go +``` + +## Key Points + +- Uses Google well-known types (`google.protobuf.Empty`, `google.protobuf.Timestamp`) directly as RPC request/response types (not just message fields). +- goctl correctly maps these to Go types (`emptypb.Empty`, `timestamppb.Timestamp`) and generates proper imports. +- This differs from Example 06 where well-known types are used as message fields. diff --git a/tools/goctl/rpc/example/09-google-types-as-rpc/service.proto b/tools/goctl/rpc/example/09-google-types-as-rpc/service.proto new file mode 100644 index 000000000..948fdd4e8 --- /dev/null +++ b/tools/goctl/rpc/example/09-google-types-as-rpc/service.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package healthsvc; + +option go_package = "example.com/demo/pb"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +message HealthCheckReq { + string service = 1; +} + +service HealthService { + // Ping returns empty — useful for health checks with no response body. + rpc Ping(HealthCheckReq) returns (google.protobuf.Empty); + // GetTime returns a Timestamp directly as the response type. + rpc GetTime(HealthCheckReq) returns (google.protobuf.Timestamp); +} diff --git a/tools/goctl/rpc/example/10-streaming/README-cn.md b/tools/goctl/rpc/example/10-streaming/README-cn.md new file mode 100644 index 000000000..59ae95096 --- /dev/null +++ b/tools/goctl/rpc/example/10-streaming/README-cn.md @@ -0,0 +1,66 @@ +# 示例 10:流式 RPC + +本示例演示 gRPC 的三种流式通信模式:服务端流、客户端流和双向流。 + +## Proto 定义 + +`stream.proto` 定义了三个 RPC 方法,演示每种流式模式。 + +`go_package` 使用完整的模块路径: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +## 生成命令 + +首先,在输出目录中初始化 `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +然后生成代码: + +```bash +goctl rpc protoc stream.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +生成的目录结构: + +``` +output/ +├── etc +│ └── streamsvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── bidistreamlogic.go +│ │ ├── clientstreamlogic.go +│ │ └── serverstreamlogic.go +│ ├── server +│ │ └── streamserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── stream.pb.go +│ └── stream_grpc.pb.go +├── streamservice +│ └── streamservice.go +└── streamsvc.go +``` + +## 要点说明 + +- 支持三种流式模式:服务端流(响应带 `stream`)、客户端流(请求带 `stream`)和双向流(两端都带 `stream`)。 +- goctl 为每个流式 RPC 方法生成独立的逻辑文件。 +- 流式客户端代码不会自动生成,需直接使用 gRPC 客户端。 diff --git a/tools/goctl/rpc/example/10-streaming/README.md b/tools/goctl/rpc/example/10-streaming/README.md new file mode 100644 index 000000000..477cdd2af --- /dev/null +++ b/tools/goctl/rpc/example/10-streaming/README.md @@ -0,0 +1,66 @@ +# Example 10: Streaming RPC + +This example demonstrates all three gRPC streaming patterns: server streaming, client streaming, and bidirectional streaming. + +## Proto Definition + +`stream.proto` defines three RPC methods demonstrating each streaming pattern. + +The `go_package` uses a full module path: + +```protobuf +option go_package = "example.com/demo/pb"; +``` + +## Generation Commands + +First, initialize the output directory with a `go.mod`: + +```bash +mkdir -p output && cd output && go mod init example.com/demo && cd .. +``` + +Then generate the code: + +```bash +goctl rpc protoc stream.proto \ + --go_out=output \ + --go-grpc_out=output \ + --zrpc_out=output \ + --go_opt=module=example.com/demo \ + --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo \ + -I . +``` + +Generated directory structure: + +``` +output/ +├── etc +│ └── streamsvc.yaml +├── go.mod +├── internal +│ ├── config +│ │ └── config.go +│ ├── logic +│ │ ├── bidistreamlogic.go +│ │ ├── clientstreamlogic.go +│ │ └── serverstreamlogic.go +│ ├── server +│ │ └── streamserviceserver.go +│ └── svc +│ └── servicecontext.go +├── pb +│ ├── stream.pb.go +│ └── stream_grpc.pb.go +├── streamservice +│ └── streamservice.go +└── streamsvc.go +``` + +## Key Points + +- Supports three streaming patterns: server streaming (`stream` on response), client streaming (`stream` on request), and bidirectional streaming (`stream` on both). +- goctl generates separate logic files for each streaming RPC method. +- Streaming client code is not auto-generated; use the gRPC client directly. diff --git a/tools/goctl/rpc/example/10-streaming/stream.proto b/tools/goctl/rpc/example/10-streaming/stream.proto new file mode 100644 index 000000000..12a528c25 --- /dev/null +++ b/tools/goctl/rpc/example/10-streaming/stream.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package streamsvc; + +option go_package = "example.com/demo/pb"; + +message StreamReq { + string input = 1; +} + +message StreamReply { + string output = 1; +} + +service StreamService { + // ServerStream: client sends one request, server returns a stream of responses. + rpc ServerStream(StreamReq) returns (stream StreamReply); + + // ClientStream: client sends a stream of requests, server returns one response. + rpc ClientStream(stream StreamReq) returns (StreamReply); + + // BidiStream: both client and server send streams of messages. + rpc BidiStream(stream StreamReq) returns (stream StreamReply); +} diff --git a/tools/goctl/rpc/example/README.md b/tools/goctl/rpc/example/README.md new file mode 100644 index 000000000..ece7b0875 --- /dev/null +++ b/tools/goctl/rpc/example/README.md @@ -0,0 +1,93 @@ +# RPC Examples + +This directory contains complete examples for all `goctl rpc` code generation scenarios. + +Each example includes: +- `.proto` source files +- `README.md` (English) and `README-cn.md` (中文) documentation + +## Examples + +| # | Directory | Scenario | Key Flags | +|---|-----------|----------|-----------| +| 01 | [01-basic](01-basic/) | Basic single service, no imports | — | +| 02 | [02-import-sibling](02-import-sibling/) | Import sibling proto file | `--proto_path=.` | +| 03 | [03-import-subdir](03-import-subdir/) | Import proto from subdirectory | `--proto_path=.` | +| 04 | [04-transitive-import](04-transitive-import/) | Transitive imports (A → B → C) | `--proto_path=.` | +| 05 | [05-multiple-services](05-multiple-services/) | Multiple services in one proto | `--multiple` | +| 06 | [06-wellknown-types](06-wellknown-types/) | Google well-known types in messages | `--proto_path=$PROTOC_INCLUDE` | +| 07 | [07-external-proto-same-pkg](07-external-proto-same-pkg/) | External proto, same `go_package` | `-I ./ext_protos` | +| 08 | [08-external-proto-diff-pkg](08-external-proto-diff-pkg/) | External proto, different `go_package` | `-I ./ext_protos` | +| 09 | [09-google-types-as-rpc](09-google-types-as-rpc/) | Google types as RPC parameters | `--proto_path=$PROTOC_INCLUDE` | +| 10 | [10-streaming](10-streaming/) | Server/client/bidirectional streaming | — | + +## Prerequisites + +- [Go](https://go.dev/) 1.22+ +- [protoc](https://github.com/protocolbuffers/protobuf/releases) (Protocol Buffers compiler) +- [protoc-gen-go](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go) and [protoc-gen-go-grpc](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc) +- [goctl](https://github.com/zeromicro/go-zero/tree/master/tools/goctl) + +## Quick Start + +```bash +# Install protoc plugins +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +# Try the basic example +cd 01-basic +mkdir -p output && cd output && go mod init example.com/demo && cd .. +goctl rpc protoc greeter.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . +``` + +--- + +# RPC 示例 + +本目录包含所有 `goctl rpc` 代码生成场景的完整示例。 + +每个示例包含: +- `.proto` 源文件 +- `README.md`(英文)和 `README-cn.md`(中文)文档 + +## 示例列表 + +| # | 目录 | 场景 | 关键标志 | +|---|------|------|---------| +| 01 | [01-basic](01-basic/) | 基础单服务,无导入 | — | +| 02 | [02-import-sibling](02-import-sibling/) | 导入同级 proto 文件 | `--proto_path=.` | +| 03 | [03-import-subdir](03-import-subdir/) | 导入子目录中的 proto | `--proto_path=.` | +| 04 | [04-transitive-import](04-transitive-import/) | 传递性导入(A → B → C) | `--proto_path=.` | +| 05 | [05-multiple-services](05-multiple-services/) | 单 proto 多服务 | `--multiple` | +| 06 | [06-wellknown-types](06-wellknown-types/) | 消息中使用 Google 标准类型 | `--proto_path=$PROTOC_INCLUDE` | +| 07 | [07-external-proto-same-pkg](07-external-proto-same-pkg/) | 外部 proto,相同 `go_package` | `-I ./ext_protos` | +| 08 | [08-external-proto-diff-pkg](08-external-proto-diff-pkg/) | 外部 proto,不同 `go_package` | `-I ./ext_protos` | +| 09 | [09-google-types-as-rpc](09-google-types-as-rpc/) | Google 类型作为 RPC 参数 | `--proto_path=$PROTOC_INCLUDE` | +| 10 | [10-streaming](10-streaming/) | 服务端/客户端/双向流 | — | + +## 前置条件 + +- [Go](https://go.dev/) 1.22+ +- [protoc](https://github.com/protocolbuffers/protobuf/releases)(Protocol Buffers 编译器) +- [protoc-gen-go](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go) 和 [protoc-gen-go-grpc](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc) +- [goctl](https://github.com/zeromicro/go-zero/tree/master/tools/goctl) + +## 快速开始 + +```bash +# 安装 protoc 插件 +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +# 试试基础示例 +cd 01-basic +mkdir -p output && cd output && go mod init example.com/demo && cd .. +goctl rpc protoc greeter.proto \ + --go_out=output --go-grpc_out=output --zrpc_out=output \ + --go_opt=module=example.com/demo --go-grpc_opt=module=example.com/demo \ + --module=example.com/demo -I . +``` diff --git a/tools/goctl/rpc/generator/call.tpl b/tools/goctl/rpc/generator/call.tpl index 27b48796d..c5f637101 100644 --- a/tools/goctl/rpc/generator/call.tpl +++ b/tools/goctl/rpc/generator/call.tpl @@ -7,6 +7,7 @@ import ( {{.pbPackage}} {{if ne .pbPackage .protoGoPackage}}{{.protoGoPackage}}{{end}} + {{.extraImports}} "github.com/zeromicro/go-zero/zrpc" "google.golang.org/grpc" diff --git a/tools/goctl/rpc/generator/gen.go b/tools/goctl/rpc/generator/gen.go index 262d72a45..116b03283 100644 --- a/tools/goctl/rpc/generator/gen.go +++ b/tools/goctl/rpc/generator/gen.go @@ -1,6 +1,7 @@ package generator import ( + "os" "path/filepath" "github.com/zeromicro/go-zero/tools/goctl/rpc/parser" @@ -35,6 +36,9 @@ type ZRpcContext struct { // NameFromFilename uses proto filename instead of package name for service naming. // Default is false (uses package name, which supports multi-proto files). NameFromFilename bool + // ProtoPaths are the directories to search for imported proto files, + // equivalent to protoc -I flags. When empty the directory of Src is used. + ProtoPaths []string } // Generate generates a rpc service, through the proto file, @@ -72,6 +76,12 @@ func (g *Generator) Generate(zctx *ZRpcContext) error { return err } + // Populate ImportedProtos so that generators can resolve dotted type references. + proto.ImportedProtos, err = resolveImportedProtos(zctx) + if err != nil { + return err + } + dirCtx, err := mkdir(projectCtx, proto, g.cfg, zctx) if err != nil { return err @@ -120,3 +130,27 @@ func (g *Generator) Generate(zctx *ZRpcContext) error { return err } + +// resolveImportedProtos builds the full list of transitively imported proto +// files for zctx and returns their parsed metadata. This mirrors the proto +// path computation in genpb.go so both use the same search paths. +func resolveImportedProtos(zctx *ZRpcContext) ([]parser.ImportedProto, error) { + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + protoPaths := make([]string, 0, len(zctx.ProtoPaths)+1) + srcDir := filepath.Dir(zctx.Src) + if !filepath.IsAbs(srcDir) { + srcDir = filepath.Join(pwd, srcDir) + } + protoPaths = append(protoPaths, srcDir) + for _, p := range zctx.ProtoPaths { + if !filepath.IsAbs(p) { + p = filepath.Join(pwd, p) + } + protoPaths = append(protoPaths, p) + } + return parser.ParseImportedProtos(zctx.Src, protoPaths) +} diff --git a/tools/goctl/rpc/generator/gencall.go b/tools/goctl/rpc/generator/gencall.go index 8381ad065..5a7bc3a8e 100644 --- a/tools/goctl/rpc/generator/gencall.go +++ b/tools/goctl/rpc/generator/gencall.go @@ -47,6 +47,7 @@ func (g *Generator) GenCall(ctx DirContext, proto parser.Proto, cfg *conf.Config func (g *Generator) genCallGroup(ctx DirContext, proto parser.Proto, cfg *conf.Config) error { dir := ctx.GetCall() head := util.GetHead(proto.Name) + pkgMap := parser.BuildProtoPackageMap(proto.ImportedProtos) for _, service := range proto.Service { childPkg, err := dir.GetChildPackage(service.Name) if err != nil { @@ -90,12 +91,13 @@ func (g *Generator) genCallGroup(ctx DirContext, proto parser.Proto, cfg *conf.C serviceName = stringx.From(service.Name + "_zrpc_client").ToCamel() } - functions, err := g.genFunction(proto.PbPackage, serviceName, service, isCallPkgSameToGrpcPkg) + extraImports := collection.NewSet[string]() + functions, err := g.genFunction(proto.PbPackage, proto.GoPackage, serviceName, service, isCallPkgSameToGrpcPkg, pkgMap, alias, extraImports) if err != nil { return err } - iFunctions, err := g.getInterfaceFuncs(proto.PbPackage, service, isCallPkgSameToGrpcPkg) + iFunctions, err := g.getInterfaceFuncs(proto.PbPackage, proto.GoPackage, service, isCallPkgSameToGrpcPkg, pkgMap, extraImports) if err != nil { return err } @@ -112,6 +114,7 @@ func (g *Generator) genCallGroup(ctx DirContext, proto parser.Proto, cfg *conf.C protoGoPackage = "" } + extraImportLines := buildExtraImportLines(extraImports) aliasKeys := alias.Keys() sort.Strings(aliasKeys) if err = util.With("shared").GoFmt(true).Parse(text).SaveTo(map[string]any{ @@ -121,6 +124,7 @@ func (g *Generator) genCallGroup(ctx DirContext, proto parser.Proto, cfg *conf.C "filePackage": childDir, "pbPackage": pbPackage, "protoGoPackage": protoGoPackage, + "extraImports": extraImportLines, "serviceName": serviceName, "functions": strings.Join(functions, pathx.NL), "interface": strings.Join(iFunctions, pathx.NL), @@ -162,13 +166,15 @@ func (g *Generator) genCallInCompatibility(ctx DirContext, proto parser.Proto, serviceName = stringx.From(service.Name + "_zrpc_client").ToCamel() } + pkgMap := parser.BuildProtoPackageMap(proto.ImportedProtos) + extraImports := collection.NewSet[string]() filename := filepath.Join(dir.Filename, fmt.Sprintf("%s.go", callFilename)) - functions, err := g.genFunction(proto.PbPackage, serviceName, service, isCallPkgSameToGrpcPkg) + functions, err := g.genFunction(proto.PbPackage, proto.GoPackage, serviceName, service, isCallPkgSameToGrpcPkg, pkgMap, alias, extraImports) if err != nil { return err } - iFunctions, err := g.getInterfaceFuncs(proto.PbPackage, service, isCallPkgSameToGrpcPkg) + iFunctions, err := g.getInterfaceFuncs(proto.PbPackage, proto.GoPackage, service, isCallPkgSameToGrpcPkg, pkgMap, extraImports) if err != nil { return err } @@ -184,6 +190,7 @@ func (g *Generator) genCallInCompatibility(ctx DirContext, proto parser.Proto, pbPackage = "" protoGoPackage = "" } + extraImportLines := buildExtraImportLines(extraImports) aliasKeys := alias.Keys() sort.Strings(aliasKeys) return util.With("shared").GoFmt(true).Parse(text).SaveTo(map[string]any{ @@ -193,6 +200,7 @@ func (g *Generator) genCallInCompatibility(ctx DirContext, proto parser.Proto, "filePackage": dir.Base, "pbPackage": pbPackage, "protoGoPackage": protoGoPackage, + "extraImports": extraImportLines, "serviceName": serviceName, "functions": strings.Join(functions, pathx.NL), "interface": strings.Join(iFunctions, pathx.NL), @@ -221,8 +229,9 @@ func getMessageName(msg proto.Message) string { return strings.Join(list, "_") } -func (g *Generator) genFunction(goPackage string, serviceName string, service parser.Service, - isCallPkgSameToGrpcPkg bool) ([]string, error) { +func (g *Generator) genFunction(goPackage, mainGoPackage, serviceName string, service parser.Service, + isCallPkgSameToGrpcPkg bool, pkgMap map[string]parser.ImportedProto, + alias, extraImports *collection.Set[string]) ([]string, error) { functions := make([]string, 0) for _, rpc := range service.RPC { @@ -238,13 +247,29 @@ func (g *Generator) genFunction(goPackage string, serviceName string, service pa streamServer = fmt.Sprintf("%s_%s%s", parser.CamelCase(service.Name), parser.CamelCase(rpc.Name), "Client") } + + reqName, reqAlias, reqImport := resolveCallTypeRef(rpc.RequestType, goPackage, mainGoPackage, pkgMap) + respName, respAlias, respImport := resolveCallTypeRef(rpc.ReturnsType, goPackage, mainGoPackage, pkgMap) + if reqAlias != "" { + alias.Add(reqAlias) + } + if respAlias != "" { + alias.Add(respAlias) + } + if reqImport != "" { + extraImports.Add(reqImport) + } + if respImport != "" { + extraImports.Add(respImport) + } + buffer, err := util.With("sharedFn").Parse(text).Execute(map[string]any{ "serviceName": serviceName, "rpcServiceName": parser.CamelCase(service.Name), "method": parser.CamelCase(rpc.Name), "package": goPackage, - "pbRequest": parser.CamelCase(rpc.RequestType), - "pbResponse": parser.CamelCase(rpc.ReturnsType), + "pbRequest": reqName, + "pbResponse": respName, "hasComment": len(comment) > 0, "comment": comment, "hasReq": !rpc.StreamsRequest, @@ -262,8 +287,9 @@ func (g *Generator) genFunction(goPackage string, serviceName string, service pa return functions, nil } -func (g *Generator) getInterfaceFuncs(goPackage string, service parser.Service, - isCallPkgSameToGrpcPkg bool) ([]string, error) { +func (g *Generator) getInterfaceFuncs(goPackage, mainGoPackage string, service parser.Service, + isCallPkgSameToGrpcPkg bool, pkgMap map[string]parser.ImportedProto, + extraImports *collection.Set[string]) ([]string, error) { functions := make([]string, 0) for _, rpc := range service.RPC { @@ -280,15 +306,25 @@ func (g *Generator) getInterfaceFuncs(goPackage string, service parser.Service, streamServer = fmt.Sprintf("%s_%s%s", parser.CamelCase(service.Name), parser.CamelCase(rpc.Name), "Client") } + + reqName, _, reqImport := resolveCallTypeRef(rpc.RequestType, goPackage, mainGoPackage, pkgMap) + respName, _, respImport := resolveCallTypeRef(rpc.ReturnsType, goPackage, mainGoPackage, pkgMap) + if reqImport != "" { + extraImports.Add(reqImport) + } + if respImport != "" { + extraImports.Add(respImport) + } + buffer, err := util.With("interfaceFn").Parse(text).Execute( map[string]any{ "hasComment": len(comment) > 0, "comment": comment, "method": parser.CamelCase(rpc.Name), "hasReq": !rpc.StreamsRequest, - "pbRequest": parser.CamelCase(rpc.RequestType), + "pbRequest": reqName, "notStream": !rpc.StreamsRequest && !rpc.StreamsReturns, - "pbResponse": parser.CamelCase(rpc.ReturnsType), + "pbResponse": respName, "streamBody": streamServer, }) if err != nil { @@ -300,3 +336,18 @@ func (g *Generator) getInterfaceFuncs(goPackage string, service parser.Service, return functions, nil } + +// buildExtraImportLines converts a set of import paths into quoted import lines +// for use in the call.tpl {{.extraImports}} placeholder. +func buildExtraImportLines(extraImports *collection.Set[string]) string { +if extraImports.Count() == 0 { +return "" +} +keys := extraImports.Keys() +sort.Strings(keys) +lines := make([]string, 0, len(keys)) +for _, k := range keys { +lines = append(lines, fmt.Sprintf(`"%s"`, k)) +} +return strings.Join(lines, "\n\t") +} diff --git a/tools/goctl/rpc/generator/genlogic.go b/tools/goctl/rpc/generator/genlogic.go index 4a51d4fb7..b2e65ee44 100644 --- a/tools/goctl/rpc/generator/genlogic.go +++ b/tools/goctl/rpc/generator/genlogic.go @@ -40,6 +40,7 @@ func (g *Generator) genLogicInCompatibility(ctx DirContext, proto parser.Proto, cfg *conf.Config) error { dir := ctx.GetLogic() service := proto.Service[0].Service.Name + pkgMap := parser.BuildProtoPackageMap(proto.ImportedProtos) for _, rpc := range proto.Service[0].RPC { logicName := fmt.Sprintf("%sLogic", stringx.From(rpc.Name).ToCamel()) logicFilename, err := format.FileNamingFormat(cfg.NamingFormat, rpc.Name+"_logic") @@ -48,14 +49,15 @@ func (g *Generator) genLogicInCompatibility(ctx DirContext, proto parser.Proto, } filename := filepath.Join(dir.Filename, logicFilename+".go") - functions, err := g.genLogicFunction(service, proto.PbPackage, logicName, rpc) + functions, err := g.genLogicFunction(service, proto.PbPackage, proto.GoPackage, logicName, rpc, pkgMap) if err != nil { return err } imports := collection.NewSet[string]() imports.Add(fmt.Sprintf(`"%v"`, ctx.GetSvc().Package)) - imports.Add(fmt.Sprintf(`"%v"`, ctx.GetPb().Package)) + addLogicImports(imports, ctx.GetPb().Package, proto.PbPackage, proto.GoPackage, rpc, pkgMap) + text, err := pathx.LoadTemplate(category, logicTemplateFileFile, logicTemplate) if err != nil { return err @@ -75,6 +77,7 @@ func (g *Generator) genLogicInCompatibility(ctx DirContext, proto parser.Proto, func (g *Generator) genLogicGroup(ctx DirContext, proto parser.Proto, cfg *conf.Config) error { dir := ctx.GetLogic() + pkgMap := parser.BuildProtoPackageMap(proto.ImportedProtos) for _, item := range proto.Service { serviceName := item.Name for _, rpc := range item.RPC { @@ -101,14 +104,15 @@ func (g *Generator) genLogicGroup(ctx DirContext, proto parser.Proto, cfg *conf. } filename = filepath.Join(dir.Filename, serviceDir, logicFilename+".go") - functions, err := g.genLogicFunction(serviceName, proto.PbPackage, logicName, rpc) + functions, err := g.genLogicFunction(serviceName, proto.PbPackage, proto.GoPackage, logicName, rpc, pkgMap) if err != nil { return err } imports := collection.NewSet[string]() imports.Add(fmt.Sprintf(`"%v"`, ctx.GetSvc().Package)) - imports.Add(fmt.Sprintf(`"%v"`, ctx.GetPb().Package)) + addLogicImports(imports, ctx.GetPb().Package, proto.PbPackage, proto.GoPackage, rpc, pkgMap) + text, err := pathx.LoadTemplate(category, logicTemplateFileFile, logicTemplate) if err != nil { return err @@ -127,9 +131,8 @@ func (g *Generator) genLogicGroup(ctx DirContext, proto parser.Proto, cfg *conf. return nil } -func (g *Generator) genLogicFunction(serviceName, goPackage, logicName string, - rpc *parser.RPC) (string, - error) { +func (g *Generator) genLogicFunction(serviceName, goPackage, mainGoPackage, logicName string, + rpc *parser.RPC, pkgMap map[string]parser.ImportedProto) (string, error) { functions := make([]string, 0) text, err := pathx.LoadTemplate(category, logicFuncTemplateFileFile, logicFunctionTemplate) if err != nil { @@ -139,14 +142,18 @@ func (g *Generator) genLogicFunction(serviceName, goPackage, logicName string, comment := parser.GetComment(rpc.Doc()) streamServer := fmt.Sprintf("%s.%s_%s%s", goPackage, parser.CamelCase(serviceName), parser.CamelCase(rpc.Name), "Server") + + reqRef := resolveRPCTypeRef(rpc.RequestType, goPackage, mainGoPackage, pkgMap) + respRef := resolveRPCTypeRef(rpc.ReturnsType, goPackage, mainGoPackage, pkgMap) + buffer, err := util.With("fun").Parse(text).Execute(map[string]any{ "logicName": logicName, "method": parser.CamelCase(rpc.Name), "hasReq": !rpc.StreamsRequest, - "request": fmt.Sprintf("*%s.%s", goPackage, parser.CamelCase(rpc.RequestType)), + "request": "*" + reqRef.GoRef, "hasReply": !rpc.StreamsRequest && !rpc.StreamsReturns, - "response": fmt.Sprintf("*%s.%s", goPackage, parser.CamelCase(rpc.ReturnsType)), - "responseType": fmt.Sprintf("%s.%s", goPackage, parser.CamelCase(rpc.ReturnsType)), + "response": "*" + respRef.GoRef, + "responseType": respRef.GoRef, "stream": rpc.StreamsRequest || rpc.StreamsReturns, "streamBody": streamServer, "hasComment": len(comment) > 0, @@ -159,3 +166,29 @@ func (g *Generator) genLogicFunction(serviceName, goPackage, logicName string, functions = append(functions, buffer.String()) return strings.Join(functions, pathx.NL), nil } + +// addLogicImports adds the correct import paths to imports for a single RPC's +// logic file. The main pb package is only included when it is actually referenced +// (i.e. when the request or response type lives in that package, or the RPC streams). +func addLogicImports(imports *collection.Set[string], pbImportPath, goPackage, mainGoPackage string, +rpc *parser.RPC, pkgMap map[string]parser.ImportedProto) { +// Streaming RPCs always reference the main pb package (for the stream type). +if rpc.StreamsRequest || rpc.StreamsReturns { +imports.Add(fmt.Sprintf(`"%s"`, pbImportPath)) +return +} + +reqRef := resolveRPCTypeRef(rpc.RequestType, goPackage, mainGoPackage, pkgMap) +respRef := resolveRPCTypeRef(rpc.ReturnsType, goPackage, mainGoPackage, pkgMap) + +// Add main pb import if any type ref is from the main package (no extra import path). +if reqRef.ImportPath == "" || respRef.ImportPath == "" { +imports.Add(fmt.Sprintf(`"%s"`, pbImportPath)) +} +if reqRef.ImportPath != "" { +imports.Add(fmt.Sprintf(`"%s"`, reqRef.ImportPath)) +} +if respRef.ImportPath != "" { +imports.Add(fmt.Sprintf(`"%s"`, respRef.ImportPath)) +} +} diff --git a/tools/goctl/rpc/generator/genpb.go b/tools/goctl/rpc/generator/genpb.go index ceb50fd9d..e1350b8ea 100644 --- a/tools/goctl/rpc/generator/genpb.go +++ b/tools/goctl/rpc/generator/genpb.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/zeromicro/go-zero/tools/goctl/rpc/execx" + "github.com/zeromicro/go-zero/tools/goctl/rpc/parser" "github.com/zeromicro/go-zero/tools/goctl/util/pathx" ) @@ -19,19 +20,89 @@ func (g *Generator) GenPb(ctx DirContext, c *ZRpcContext) error { } func (g *Generator) genPbDirect(ctx DirContext, c *ZRpcContext) error { - g.log.Debug("[command]: %s", c.ProtocCmd) pwd, err := os.Getwd() if err != nil { return err } - _, err = execx.Run(c.ProtocCmd, pwd) + protocCmd, err := g.buildProtocCmd(c, pwd) + if err != nil { + return err + } + + g.log.Debug("[command]: %s", protocCmd) + _, err = execx.Run(protocCmd, pwd) if err != nil { return err } return g.setPbDir(ctx, c) } +// buildProtocCmd resolves all transitively imported proto files and appends +// them to the protoc command so that their pb.go files are also generated. +func (g *Generator) buildProtocCmd(c *ZRpcContext, pwd string) (string, error) { + // Build the full list of proto search paths (absolute). + protoPaths := make([]string, 0, len(c.ProtoPaths)+1) + + // Always include the directory of the source proto so that imports + // relative to the source file can be found. + srcDir := filepath.Dir(c.Src) + if !filepath.IsAbs(srcDir) { + srcDir = filepath.Join(pwd, srcDir) + } + protoPaths = append(protoPaths, srcDir) + + for _, p := range c.ProtoPaths { + if !filepath.IsAbs(p) { + p = filepath.Join(pwd, p) + } + protoPaths = append(protoPaths, p) + } + + importedFiles, err := parser.ResolveImports(c.Src, protoPaths) + if err != nil { + return "", err + } + if len(importedFiles) == 0 { + return c.ProtocCmd, nil + } + + cmd := c.ProtocCmd + for _, f := range importedFiles { + // Use the path relative to the best-matching --proto_path entry so that + // protoc's source_relative output lands in the correct directory. + // e.g. if --proto_path=./ext and the file is ext/common/types.proto, + // we pass "common/types.proto" rather than "ext/common/types.proto". + rel := relativeToProtoPath(f, protoPaths, pwd) + cmd += " " + rel + } + return cmd, nil +} + +// relativeToProtoPath returns the path of f relative to the most specific +// (longest) proto_path entry that is a parent of f. Falls back to relative +// to pwd when no proto_path matches. +func relativeToProtoPath(f string, protoPaths []string, pwd string) string { + bestRel := "" + bestLen := 0 + for _, pp := range protoPaths { + prefix := pp + string(filepath.Separator) + if strings.HasPrefix(f, prefix) && len(pp) > bestLen { + if rel, err := filepath.Rel(pp, f); err == nil { + bestRel = rel + bestLen = len(pp) + } + } + } + if bestRel != "" { + return bestRel + } + if rel, err := filepath.Rel(pwd, f); err == nil { + return rel + } + return f +} + func (g *Generator) setPbDir(ctx DirContext, c *ZRpcContext) error { pbDir, err := findPbFile(c.GoOutput, c.Src, false) if err != nil { diff --git a/tools/goctl/rpc/generator/genserver.go b/tools/goctl/rpc/generator/genserver.go index c24b11426..276cb8524 100644 --- a/tools/goctl/rpc/generator/genserver.go +++ b/tools/goctl/rpc/generator/genserver.go @@ -38,6 +38,7 @@ func (g *Generator) GenServer(ctx DirContext, proto parser.Proto, cfg *conf.Conf func (g *Generator) genServerGroup(ctx DirContext, proto parser.Proto, cfg *conf.Config) error { dir := ctx.GetServer() + pkgMap := parser.BuildProtoPackageMap(proto.ImportedProtos) for _, service := range proto.Service { var ( serverFile string @@ -71,10 +72,13 @@ func (g *Generator) genServerGroup(ctx DirContext, proto parser.Proto, cfg *conf head := util.GetHead(proto.Name) - funcList, err := g.genFunctions(proto.PbPackage, service, true) + funcList, extraImportPaths, err := g.genFunctions(proto.PbPackage, proto.GoPackage, service, true, pkgMap) if err != nil { return err } + for _, imp := range extraImportPaths { + imports.Add(fmt.Sprintf(`"%s"`, imp)) + } text, err := pathx.LoadTemplate(category, serverTemplateFile, serverTemplate) if err != nil { @@ -114,6 +118,7 @@ func (g *Generator) genServerInCompatibility(ctx DirContext, proto parser.Proto, imports := collection.NewSet[string]() imports.Add(logicImport, svcImport, pbImport) + pkgMap := parser.BuildProtoPackageMap(proto.ImportedProtos) head := util.GetHead(proto.Name) service := proto.Service[0] serverFilename, err := format.FileNamingFormat(cfg.NamingFormat, service.Name+"_server") @@ -122,10 +127,13 @@ func (g *Generator) genServerInCompatibility(ctx DirContext, proto parser.Proto, } serverFile := filepath.Join(dir.Filename, serverFilename+".go") - funcList, err := g.genFunctions(proto.PbPackage, service, false) + funcList, extraImportPaths, err := g.genFunctions(proto.PbPackage, proto.GoPackage, service, false, pkgMap) if err != nil { return err } + for _, imp := range extraImportPaths { + imports.Add(fmt.Sprintf(`"%s"`, imp)) + } text, err := pathx.LoadTemplate(category, serverTemplateFile, serverTemplate) if err != nil { @@ -151,15 +159,17 @@ func (g *Generator) genServerInCompatibility(ctx DirContext, proto parser.Proto, }, serverFile, true) } -func (g *Generator) genFunctions(goPackage string, service parser.Service, multiple bool) ([]string, error) { +func (g *Generator) genFunctions(goPackage, mainGoPackage string, service parser.Service, + multiple bool, pkgMap map[string]parser.ImportedProto) ([]string, []string, error) { var ( functionList []string logicPkg string + extraImports []string ) for _, rpc := range service.RPC { text, err := pathx.LoadTemplate(category, serverFuncTemplateFile, functionTemplate) if err != nil { - return nil, err + return nil, nil, err } var logicName string @@ -175,12 +185,22 @@ func (g *Generator) genFunctions(goPackage string, service parser.Service, multi comment := parser.GetComment(rpc.Doc()) streamServer := fmt.Sprintf("%s.%s_%s%s", goPackage, parser.CamelCase(service.Name), parser.CamelCase(rpc.Name), "Server") + + reqRef := resolveRPCTypeRef(rpc.RequestType, goPackage, mainGoPackage, pkgMap) + respRef := resolveRPCTypeRef(rpc.ReturnsType, goPackage, mainGoPackage, pkgMap) + if reqRef.ImportPath != "" { + extraImports = append(extraImports, reqRef.ImportPath) + } + if respRef.ImportPath != "" { + extraImports = append(extraImports, respRef.ImportPath) + } + buffer, err := util.With("func").Parse(text).Execute(map[string]any{ "server": stringx.From(service.Name).ToCamel(), "logicName": logicName, "method": parser.CamelCase(rpc.Name), - "request": fmt.Sprintf("*%s.%s", goPackage, parser.CamelCase(rpc.RequestType)), - "response": fmt.Sprintf("*%s.%s", goPackage, parser.CamelCase(rpc.ReturnsType)), + "request": "*" + reqRef.GoRef, + "response": "*" + respRef.GoRef, "hasComment": len(comment) > 0, "comment": comment, "hasReq": !rpc.StreamsRequest, @@ -190,10 +210,10 @@ func (g *Generator) genFunctions(goPackage string, service parser.Service, multi "logicPkg": logicPkg, }) if err != nil { - return nil, err + return nil, nil, err } functionList = append(functionList, buffer.String()) } - return functionList, nil + return functionList, extraImports, nil } diff --git a/tools/goctl/rpc/generator/typeref.go b/tools/goctl/rpc/generator/typeref.go new file mode 100644 index 000000000..453972702 --- /dev/null +++ b/tools/goctl/rpc/generator/typeref.go @@ -0,0 +1,115 @@ +package generator + +import ( + "fmt" + "strings" + + "github.com/zeromicro/go-zero/tools/goctl/rpc/parser" +) + +// rpcTypeRef holds the resolved Go type reference for an RPC request/response type. +type rpcTypeRef struct { + // GoRef is the qualified Go type name, e.g. "pb.GetReq", "common.TypesReq", "emptypb.Empty". + GoRef string + // ImportPath is an extra Go import path required for cross-package types. + // Empty when the type lives in a package that is already imported. + ImportPath string +} + +// resolveRPCTypeRef resolves a proto RPC type (possibly dotted) to its Go type +// reference and the optional extra import it requires. +// +// - Simple types ("GetReq") → mainPbPackage.GetReq, no extra import. +// - Same-package dotted types ("ext.ExtReq", same go_package) → mainPbPackage.ExtReq. +// - Cross-package dotted types ("common.TypesReq") → common.TypesReq + import path. +// - google.protobuf.X types → well-known Go type + import path. +func resolveRPCTypeRef(protoType, mainPbPackage, mainGoPackage string, pkgMap map[string]parser.ImportedProto) rpcTypeRef { + if !strings.Contains(protoType, ".") { + return rpcTypeRef{GoRef: fmt.Sprintf("%s.%s", mainPbPackage, parser.CamelCase(protoType))} + } + + if strings.HasPrefix(protoType, "google.protobuf.") { + typeName := strings.TrimPrefix(protoType, "google.protobuf.") + return resolveGoogleWKT(typeName) + } + + dot := strings.Index(protoType, ".") + protoPkg, typeName := protoType[:dot], protoType[dot+1:] + + if imp, ok := pkgMap[protoPkg]; ok { + camelType := parser.CamelCase(typeName) + if imp.GoPackage == mainGoPackage { + // Same Go package as main proto — no extra import needed. + return rpcTypeRef{GoRef: fmt.Sprintf("%s.%s", mainPbPackage, camelType)} + } + return rpcTypeRef{ + GoRef: fmt.Sprintf("%s.%s", imp.PbPackage, camelType), + ImportPath: imp.GoPackage, + } + } + + // Fallback: treat as same package with CamelCase applied to full dotted name. + return rpcTypeRef{GoRef: fmt.Sprintf("%s.%s", mainPbPackage, parser.CamelCase(protoType))} +} + +// resolveCallTypeRef is tailored for gencall.go's type-alias system. +// Returns: +// - typeName: the identifier to place in function signatures (alias name or full "pkg.Type" ref). +// - aliasEntry: if non-empty, a "TypeName = pkg.TypeName" alias declaration to add to the type block. +// - importPath: if non-empty, the extra import path needed. +func resolveCallTypeRef(protoType, mainPbPackage, mainGoPackage string, pkgMap map[string]parser.ImportedProto) (typeName, aliasEntry, importPath string) { + if !strings.Contains(protoType, ".") { + // Simple type — alias is produced by the existing proto.Message loop. + return parser.CamelCase(protoType), "", "" + } + + if strings.HasPrefix(protoType, "google.protobuf.") { + tn := strings.TrimPrefix(protoType, "google.protobuf.") + ref := resolveGoogleWKT(tn) + return ref.GoRef, "", ref.ImportPath + } + + dot := strings.Index(protoType, ".") + protoPkg, tn := protoType[:dot], protoType[dot+1:] + camelType := parser.CamelCase(tn) + + if imp, ok := pkgMap[protoPkg]; ok { + if imp.GoPackage == mainGoPackage { + // Same Go package: add an alias so the function signature uses a simple name. + entry := fmt.Sprintf("%s = %s.%s", camelType, mainPbPackage, camelType) + return camelType, entry, "" + } + // Different Go package: use fully-qualified ref directly; no alias needed. + return fmt.Sprintf("%s.%s", imp.PbPackage, camelType), "", imp.GoPackage + } + + return parser.CamelCase(protoType), "", "" +} + +// googleWKTTable maps google.protobuf type names to their generated Go equivalents. +var googleWKTTable = map[string]rpcTypeRef{ + "Empty": {GoRef: "emptypb.Empty", ImportPath: "google.golang.org/protobuf/types/known/emptypb"}, + "Timestamp": {GoRef: "timestamppb.Timestamp", ImportPath: "google.golang.org/protobuf/types/known/timestamppb"}, + "Duration": {GoRef: "durationpb.Duration", ImportPath: "google.golang.org/protobuf/types/known/durationpb"}, + "Any": {GoRef: "anypb.Any", ImportPath: "google.golang.org/protobuf/types/known/anypb"}, + "StringValue": {GoRef: "wrapperspb.StringValue", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "Int32Value": {GoRef: "wrapperspb.Int32Value", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "Int64Value": {GoRef: "wrapperspb.Int64Value", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "BoolValue": {GoRef: "wrapperspb.BoolValue", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "BytesValue": {GoRef: "wrapperspb.BytesValue", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "FloatValue": {GoRef: "wrapperspb.FloatValue", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "DoubleValue": {GoRef: "wrapperspb.DoubleValue", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "UInt32Value": {GoRef: "wrapperspb.UInt32Value", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "UInt64Value": {GoRef: "wrapperspb.UInt64Value", ImportPath: "google.golang.org/protobuf/types/known/wrapperspb"}, + "Struct": {GoRef: "structpb.Struct", ImportPath: "google.golang.org/protobuf/types/known/structpb"}, + "Value": {GoRef: "structpb.Value", ImportPath: "google.golang.org/protobuf/types/known/structpb"}, + "ListValue": {GoRef: "structpb.ListValue", ImportPath: "google.golang.org/protobuf/types/known/structpb"}, + "FieldMask": {GoRef: "fieldmaskpb.FieldMask", ImportPath: "google.golang.org/protobuf/types/known/fieldmaskpb"}, +} + +func resolveGoogleWKT(typeName string) rpcTypeRef { + if r, ok := googleWKTTable[typeName]; ok { + return r + } + return rpcTypeRef{GoRef: "interface{}"} +} diff --git a/tools/goctl/rpc/parser/base.proto b/tools/goctl/rpc/parser/base.proto new file mode 100644 index 000000000..57087cdc5 --- /dev/null +++ b/tools/goctl/rpc/parser/base.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package base; + +option go_package = "github.com/zeromicro/go-zero/tools/goctl/rpc/parser/base"; + +message BaseMessage { + string id = 1; +} diff --git a/tools/goctl/rpc/parser/import.go b/tools/goctl/rpc/parser/import.go index bb9591f56..bd53f095a 100644 --- a/tools/goctl/rpc/parser/import.go +++ b/tools/goctl/rpc/parser/import.go @@ -1,8 +1,194 @@ package parser -import "github.com/emicklei/proto" +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/emicklei/proto" +) // Import embeds proto.Import type Import struct { *proto.Import } + +// ImportedProto holds the package information of a transitively imported proto file. +type ImportedProto struct { + // Src is the absolute path to the proto file. + Src string + // ProtoPackage is the value of the proto "package" declaration. + // It is the qualifier used in dotted type references, e.g. "ext" in "ext.ExtReq". + ProtoPackage string + // GoPackage is the value of the option go_package field, or the proto + // package name when go_package is absent. + GoPackage string + // PbPackage is the sanitized Go package name derived from GoPackage. + PbPackage string +} + +// BuildProtoPackageMap returns a map from proto package name to ImportedProto, +// enabling O(1) lookup of Go package info given a proto type qualifier like "ext". +func BuildProtoPackageMap(importedProtos []ImportedProto) map[string]ImportedProto { + m := make(map[string]ImportedProto, len(importedProtos)) + for _, imp := range importedProtos { + if imp.ProtoPackage != "" { + m[imp.ProtoPackage] = imp + } + } + return m +} + +// ResolveImports returns the absolute paths of all transitively imported proto +// files reachable from src, excluding well-known types (google/*). +// It searches for imported files in protoPaths (equivalent to protoc -I flags). +// Files that cannot be found in protoPaths are silently skipped so that +// system-level or well-known protos do not cause errors. +func ResolveImports(src string, protoPaths []string) ([]string, error) { + absSrc, err := filepath.Abs(src) + if err != nil { + return nil, err + } + + visited := make(map[string]bool) + visited[absSrc] = true // exclude the source itself from the result + var result []string + if err := collectImports(absSrc, protoPaths, visited, &result); err != nil { + return nil, err + } + return result, nil +} + +// ParseImportedProtos resolves and parses all transitively imported proto +// files, returning their package information for use in code generation. +func ParseImportedProtos(src string, protoPaths []string) ([]ImportedProto, error) { + paths, err := ResolveImports(src, protoPaths) + if err != nil { + return nil, err + } + + result := make([]ImportedProto, 0, len(paths)) + for _, p := range paths { + goPackage, pbPackage, protoPackage, err := parseGoPackage(p) + if err != nil { + return nil, err + } + result = append(result, ImportedProto{ + Src: p, + ProtoPackage: protoPackage, + GoPackage: goPackage, + PbPackage: pbPackage, + }) + } + return result, nil +} + +// collectImports recursively walks import declarations of src, appending newly +// discovered absolute proto file paths to result. +func collectImports(src string, protoPaths []string, visited map[string]bool, result *[]string) error { + importFilenames, err := parseImportFilenames(src) + if err != nil { + return err + } + + for _, filename := range importFilenames { + if isWellKnownProto(filename) { + continue + } + + abs, err := lookupProtoFile(filename, protoPaths) + if err != nil { + // Not found in the provided proto paths — may be a system-level proto. + // Skip rather than fail, mirroring protoc's own behaviour. + continue + } + + if visited[abs] { + continue + } + visited[abs] = true + *result = append(*result, abs) + + if err := collectImports(abs, protoPaths, visited, result); err != nil { + return err + } + } + return nil +} + +// parseImportFilenames opens src and returns the Filename field of every +// import statement without performing any file-system lookups. +func parseImportFilenames(src string) ([]string, error) { + r, err := os.Open(src) + if err != nil { + return nil, err + } + defer r.Close() + + p := proto.NewParser(r) + set, err := p.Parse() + if err != nil { + return nil, err + } + + var imports []string + proto.Walk(set, proto.WithImport(func(i *proto.Import) { + imports = append(imports, i.Filename) + })) + return imports, nil +} + +// parseGoPackage reads only the go_package option and package declaration from +// src, returning the derived GoPackage, PbPackage, and ProtoPackage without +// requiring a service definition (imported protos often have no service block). +func parseGoPackage(src string) (goPackage, pbPackage, protoPackage string, err error) { + r, err := os.Open(src) + if err != nil { + return "", "", "", err + } + defer r.Close() + + p := proto.NewParser(r) + set, err := p.Parse() + if err != nil { + return "", "", "", err + } + + var packageName string + proto.Walk(set, + proto.WithOption(func(opt *proto.Option) { + if opt.Name == "go_package" { + goPackage = opt.Constant.Source + } + }), + proto.WithPackage(func(pkg *proto.Package) { + packageName = pkg.Name + }), + ) + + if len(goPackage) == 0 { + goPackage = packageName + } + pbPackage = GoSanitized(filepath.Base(goPackage)) + protoPackage = packageName + return goPackage, pbPackage, protoPackage, nil +} + +// lookupProtoFile searches for filename inside each directory of protoPaths, +// returning its absolute path on the first match. +func lookupProtoFile(filename string, protoPaths []string) (string, error) { + for _, dir := range protoPaths { + candidate := filepath.Join(dir, filename) + if _, err := os.Stat(candidate); err == nil { + return filepath.Abs(candidate) + } + } + return "", fmt.Errorf("proto file %q not found in proto paths %v", filename, protoPaths) +} + +// isWellKnownProto reports whether filename refers to a well-known type +// bundled with protoc (e.g. google/protobuf/timestamp.proto). +func isWellKnownProto(filename string) bool { + return strings.HasPrefix(filename, "google/") +} diff --git a/tools/goctl/rpc/parser/import_test.go b/tools/goctl/rpc/parser/import_test.go new file mode 100644 index 000000000..fed78d4de --- /dev/null +++ b/tools/goctl/rpc/parser/import_test.go @@ -0,0 +1,107 @@ +package parser + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveImports_Basic(t *testing.T) { + // test.proto imports "base.proto", which lives in the same directory. + absDir, err := filepath.Abs(".") + assert.NoError(t, err) + + paths, err := ResolveImports("./test.proto", []string{absDir}) + assert.NoError(t, err) + assert.Len(t, paths, 1) + assert.True(t, strings.HasSuffix(paths[0], "base.proto")) + assert.True(t, filepath.IsAbs(paths[0])) +} + +func TestResolveImports_SourceExcluded(t *testing.T) { + // The source file itself must not appear in the result. + absDir, err := filepath.Abs(".") + assert.NoError(t, err) + + absSrc, err := filepath.Abs("./test.proto") + assert.NoError(t, err) + + paths, err := ResolveImports("./test.proto", []string{absDir}) + assert.NoError(t, err) + for _, p := range paths { + assert.NotEqual(t, absSrc, p) + } +} + +func TestResolveImports_NotFound(t *testing.T) { + // Imports that cannot be located in protoPaths are silently skipped. + paths, err := ResolveImports("./test.proto", []string{"/nonexistent/path"}) + assert.NoError(t, err) + assert.Empty(t, paths) +} + +func TestResolveImports_NoDuplicates(t *testing.T) { + // Even if the same proto is found via multiple search paths, it should + // appear only once. + absDir, err := filepath.Abs(".") + assert.NoError(t, err) + + paths, err := ResolveImports("./test.proto", []string{absDir, absDir}) + assert.NoError(t, err) + + seen := make(map[string]int) + for _, p := range paths { + seen[p]++ + } + for p, count := range seen { + assert.Equal(t, 1, count, "duplicate path: %s", p) + } +} + +func TestParseImportedProtos_Basic(t *testing.T) { + absDir, err := filepath.Abs(".") + assert.NoError(t, err) + + protos, err := ParseImportedProtos("./test.proto", []string{absDir}) + assert.NoError(t, err) + assert.Len(t, protos, 1) + + imp := protos[0] + assert.Equal(t, "github.com/zeromicro/go-zero/tools/goctl/rpc/parser/base", imp.GoPackage) + assert.Equal(t, "base", imp.PbPackage) + assert.True(t, filepath.IsAbs(imp.Src)) +} + +func TestParseImportedProtos_EmptyWhenNoImports(t *testing.T) { + // test_option.proto has no imports, so the result should be empty. + absDir, err := filepath.Abs(".") + assert.NoError(t, err) + + protos, err := ParseImportedProtos("./test_option.proto", []string{absDir}) + assert.NoError(t, err) + assert.Empty(t, protos) +} + +func TestIsWellKnownProto(t *testing.T) { + assert.True(t, isWellKnownProto("google/protobuf/timestamp.proto")) + assert.True(t, isWellKnownProto("google/protobuf/empty.proto")) + assert.False(t, isWellKnownProto("base.proto")) + assert.False(t, isWellKnownProto("common/types.proto")) +} + +func TestLookupProtoFile_Found(t *testing.T) { + absDir, err := filepath.Abs(".") + assert.NoError(t, err) + + got, err := lookupProtoFile("base.proto", []string{absDir}) + assert.NoError(t, err) + assert.True(t, filepath.IsAbs(got)) + assert.True(t, strings.HasSuffix(got, "base.proto")) +} + +func TestLookupProtoFile_NotFound(t *testing.T) { + _, err := lookupProtoFile("nonexistent.proto", []string{"/no/such/dir"}) + assert.Error(t, err) +} diff --git a/tools/goctl/rpc/parser/parser_test.go b/tools/goctl/rpc/parser/parser_test.go index 4375b0599..bd8b204bc 100644 --- a/tools/goctl/rpc/parser/parser_test.go +++ b/tools/goctl/rpc/parser/parser_test.go @@ -2,7 +2,6 @@ package parser import ( "sort" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -44,20 +43,21 @@ func TestDefaultProtoParse(t *testing.T) { }()) } -func TestDefaultProtoParseCaseInvalidRequestType(t *testing.T) { +func TestDefaultProtoParseDottedRequestType(t *testing.T) { + // Dotted types (e.g. "base.Req") are now valid — they refer to messages in + // imported protos. Parsing should succeed. p := NewDefaultProtoParser() - _, err := p.Parse("./test_invalid_request.proto") - assert.True(t, true, func() bool { - return strings.Contains(err.Error(), "request type must defined in") - }()) + data, err := p.Parse("./test_invalid_request.proto") + assert.NoError(t, err) + assert.Equal(t, "base.Req", data.Service[0].RPC[0].RequestType) } -func TestDefaultProtoParseCaseInvalidResponseType(t *testing.T) { +func TestDefaultProtoParseDottedResponseType(t *testing.T) { + // Dotted return types (e.g. "base.Reply") are now valid. p := NewDefaultProtoParser() - _, err := p.Parse("./test_invalid_response.proto") - assert.True(t, true, func() bool { - return strings.Contains(err.Error(), "response type must defined in") - }()) + data, err := p.Parse("./test_invalid_response.proto") + assert.NoError(t, err) + assert.Equal(t, "base.Reply", data.Service[0].RPC[0].ReturnsType) } func TestDefaultProtoParseError(t *testing.T) { diff --git a/tools/goctl/rpc/parser/proto.go b/tools/goctl/rpc/parser/proto.go index a08137bff..4d11e01bc 100644 --- a/tools/goctl/rpc/parser/proto.go +++ b/tools/goctl/rpc/parser/proto.go @@ -2,12 +2,15 @@ package parser // Proto describes a proto file, type Proto struct { - Src string - Name string - Package Package - PbPackage string - GoPackage string - Import []Import - Message []Message - Service Services + Src string + Name string + Package Package + PbPackage string + GoPackage string + Import []Import + Message []Message + Service Services + // ImportedProtos holds the metadata for all transitively imported proto files. + // Populated by the generator before code generation. + ImportedProtos []ImportedProto } diff --git a/tools/goctl/rpc/parser/service.go b/tools/goctl/rpc/parser/service.go index b060e2fd3..b1829ddd4 100644 --- a/tools/goctl/rpc/parser/service.go +++ b/tools/goctl/rpc/parser/service.go @@ -2,9 +2,6 @@ package parser import ( "errors" - "fmt" - "path/filepath" - "strings" "github.com/emicklei/proto" ) @@ -35,20 +32,5 @@ func (s Services) validate(filename string, multipleOpt ...bool) error { return errors.New("only one service expected") } - name := filepath.Base(filename) - for _, service := range s { - for _, rpc := range service.RPC { - if strings.Contains(rpc.RequestType, ".") { - return fmt.Errorf("line %v:%v, request type must defined in %s", - rpc.Position.Line, - rpc.Position.Column, name) - } - if strings.Contains(rpc.ReturnsType, ".") { - return fmt.Errorf("line %v:%v, returns type must defined in %s", - rpc.Position.Line, - rpc.Position.Column, name) - } - } - } return nil } diff --git a/tools/goctl/rpc/test/.gitignore b/tools/goctl/rpc/test/.gitignore new file mode 100644 index 000000000..ea5437986 --- /dev/null +++ b/tools/goctl/rpc/test/.gitignore @@ -0,0 +1,4 @@ +# Ignore generated output from test scripts +proto/*/output/ +proto/*/output_old/ +proto/*/output_new/ diff --git a/tools/goctl/rpc/test/proto/01_no_import/compare.sh b/tools/goctl/rpc/test/proto/01_no_import/compare.sh new file mode 100755 index 000000000..04477f646 --- /dev/null +++ b/tools/goctl/rpc/test/proto/01_no_import/compare.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Scenario 01: compare old vs new goctl output — no imports +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s01_no_import > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$OLD_GOCTL" rpc protoc greeter.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_OLD" "old" + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s01_no_import > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc greeter.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/01_no_import/gen.sh b/tools/goctl/rpc/test/proto/01_no_import/gen.sh new file mode 100755 index 000000000..479ce0c5f --- /dev/null +++ b/tools/goctl/rpc/test/proto/01_no_import/gen.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Scenario 01: no imports +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s01_no_import > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc greeter.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/01_no_import/greeter.proto b/tools/goctl/rpc/test/proto/01_no_import/greeter.proto new file mode 100644 index 000000000..167359766 --- /dev/null +++ b/tools/goctl/rpc/test/proto/01_no_import/greeter.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +// 场景01:无任何 import,验证基础向后兼容性 +package greeter; + +option go_package = "example.com/demo/greeter"; + +message HelloReq { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +service Greeter { + rpc Hello(HelloReq) returns (HelloReply); + rpc SayHello(HelloReq) returns (HelloReply); +} diff --git a/tools/goctl/rpc/test/proto/02_import_sibling/compare.sh b/tools/goctl/rpc/test/proto/02_import_sibling/compare.sh new file mode 100755 index 000000000..58b10da69 --- /dev/null +++ b/tools/goctl/rpc/test/proto/02_import_sibling/compare.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Scenario 02: compare old vs new goctl output — sibling import +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s02_import_sibling > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$OLD_GOCTL" rpc protoc user.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_OLD" "old" + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s02_import_sibling > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc user.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/02_import_sibling/gen.sh b/tools/goctl/rpc/test/proto/02_import_sibling/gen.sh new file mode 100755 index 000000000..001913ee7 --- /dev/null +++ b/tools/goctl/rpc/test/proto/02_import_sibling/gen.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Scenario 02: sibling import +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s02_import_sibling > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc user.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/02_import_sibling/types.proto b/tools/goctl/rpc/test/proto/02_import_sibling/types.proto new file mode 100644 index 000000000..ed550cfad --- /dev/null +++ b/tools/goctl/rpc/test/proto/02_import_sibling/types.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +// 场景02:被 import 的共享类型定义文件 +package types; + +option go_package = "example.com/demo/s02_import_sibling/pb"; + +message User { + string id = 1; + string name = 2; + int32 age = 3; +} diff --git a/tools/goctl/rpc/test/proto/02_import_sibling/user.proto b/tools/goctl/rpc/test/proto/02_import_sibling/user.proto new file mode 100644 index 000000000..8fe3a0726 --- /dev/null +++ b/tools/goctl/rpc/test/proto/02_import_sibling/user.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +// 场景02:主 proto,import 同目录下的 types.proto +// 预期:goctl 应同时生成 types.pb.go 和 user.pb.go +package usersvc; + +option go_package = "example.com/demo/s02_import_sibling/pb"; + +import "types.proto"; + +message GetUserReq { + string id = 1; +} + +message GetUserReply { + types.User user = 1; +} + +message CreateUserReq { + string name = 1; + int32 age = 2; +} + +message CreateUserReply { + types.User user = 1; +} + +service UserService { + rpc GetUser(GetUserReq) returns (GetUserReply); + rpc CreateUser(CreateUserReq) returns (CreateUserReply); +} diff --git a/tools/goctl/rpc/test/proto/03_import_subdir/common/types.proto b/tools/goctl/rpc/test/proto/03_import_subdir/common/types.proto new file mode 100644 index 000000000..bc0bb8306 --- /dev/null +++ b/tools/goctl/rpc/test/proto/03_import_subdir/common/types.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +// 场景03:被 import 的子目录公共类型 +package common; + +option go_package = "example.com/demo/s03_import_subdir/pb/common"; + +message PageInfo { + int32 page = 1; + int32 size = 2; +} + +message SortInfo { + string field = 1; + string order = 2; +} diff --git a/tools/goctl/rpc/test/proto/03_import_subdir/compare.sh b/tools/goctl/rpc/test/proto/03_import_subdir/compare.sh new file mode 100755 index 000000000..54b8a4d03 --- /dev/null +++ b/tools/goctl/rpc/test/proto/03_import_subdir/compare.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Scenario 03: compare old vs new goctl output — subdirectory import +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s03_import_subdir > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$OLD_GOCTL" rpc protoc order.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_OLD" "old" + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s03_import_subdir > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc order.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/03_import_subdir/gen.sh b/tools/goctl/rpc/test/proto/03_import_subdir/gen.sh new file mode 100755 index 000000000..7f9cd83ba --- /dev/null +++ b/tools/goctl/rpc/test/proto/03_import_subdir/gen.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Scenario 03: subdirectory import +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s03_import_subdir > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc order.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/03_import_subdir/order.proto b/tools/goctl/rpc/test/proto/03_import_subdir/order.proto new file mode 100644 index 000000000..8a6cea0ee --- /dev/null +++ b/tools/goctl/rpc/test/proto/03_import_subdir/order.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +// 场景03:import 子目录下的 proto +// 预期:goctl 应同时生成 common/types.pb.go 和 order.pb.go +package ordersvc; + +option go_package = "example.com/demo/s03_import_subdir/pb"; + +import "common/types.proto"; + +message OrderItem { + string id = 1; + string name = 2; + double price = 3; +} + +message ListOrdersReq { + common.PageInfo page = 1; + common.SortInfo sort = 2; +} + +message ListOrdersReply { + repeated OrderItem orders = 1; +} + +message GetOrderReq { + string id = 1; +} + +message GetOrderReply { + OrderItem order = 1; +} + +service OrderService { + rpc ListOrders(ListOrdersReq) returns (ListOrdersReply); + rpc GetOrder(GetOrderReq) returns (GetOrderReply); +} diff --git a/tools/goctl/rpc/test/proto/04_transitive/base.proto b/tools/goctl/rpc/test/proto/04_transitive/base.proto new file mode 100644 index 000000000..b77c520cd --- /dev/null +++ b/tools/goctl/rpc/test/proto/04_transitive/base.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +// 场景04:最底层基础类型(C 层) +package base; + +option go_package = "example.com/demo/s04_transitive/pb"; + +message BaseResp { + int32 code = 1; + string msg = 2; +} diff --git a/tools/goctl/rpc/test/proto/04_transitive/compare.sh b/tools/goctl/rpc/test/proto/04_transitive/compare.sh new file mode 100755 index 000000000..36aebcc51 --- /dev/null +++ b/tools/goctl/rpc/test/proto/04_transitive/compare.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Scenario 04: compare old vs new goctl output — transitive imports +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s04_transitive > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$OLD_GOCTL" rpc protoc main.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_OLD" "old" + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s04_transitive > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc main.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/04_transitive/gen.sh b/tools/goctl/rpc/test/proto/04_transitive/gen.sh new file mode 100755 index 000000000..384034df8 --- /dev/null +++ b/tools/goctl/rpc/test/proto/04_transitive/gen.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Scenario 04: transitive imports +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s04_transitive > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc main.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/04_transitive/main.proto b/tools/goctl/rpc/test/proto/04_transitive/main.proto new file mode 100644 index 000000000..4d1567295 --- /dev/null +++ b/tools/goctl/rpc/test/proto/04_transitive/main.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +// 场景04:主 proto(A 层),只 import middleware.proto +// 传递依赖:A → B(middleware) → C(base) +// 预期:goctl 应同时生成 base.pb.go、middleware.pb.go、main.pb.go +package pingsvc; + +option go_package = "example.com/demo/s04_transitive/pb"; + +import "middleware.proto"; + +message PingReq { + middleware.RequestMeta meta = 1; +} + +message PingReply { + string pong = 1; +} + +service PingService { + rpc Ping(PingReq) returns (PingReply); +} diff --git a/tools/goctl/rpc/test/proto/04_transitive/middleware.proto b/tools/goctl/rpc/test/proto/04_transitive/middleware.proto new file mode 100644 index 000000000..242a5a42f --- /dev/null +++ b/tools/goctl/rpc/test/proto/04_transitive/middleware.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +// 场景04:中间层(B 层),import base.proto +package middleware; + +option go_package = "example.com/demo/s04_transitive/pb"; + +import "base.proto"; + +message RequestMeta { + string trace_id = 1; + base.BaseResp base = 2; +} diff --git a/tools/goctl/rpc/test/proto/05_multiple/compare.sh b/tools/goctl/rpc/test/proto/05_multiple/compare.sh new file mode 100755 index 000000000..e961d0f5c --- /dev/null +++ b/tools/goctl/rpc/test/proto/05_multiple/compare.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Scenario 05: compare old vs new goctl output — multiple services (--multiple mode) +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s05_multiple > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$OLD_GOCTL" rpc protoc multi.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --multiple +verify_build "$OUT_OLD" "old" + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s05_multiple > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc multi.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --multiple +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/05_multiple/gen.sh b/tools/goctl/rpc/test/proto/05_multiple/gen.sh new file mode 100755 index 000000000..772a834b5 --- /dev/null +++ b/tools/goctl/rpc/test/proto/05_multiple/gen.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Scenario 05: multiple services (--multiple mode) +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s05_multiple > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc multi.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --multiple + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/05_multiple/multi.proto b/tools/goctl/rpc/test/proto/05_multiple/multi.proto new file mode 100644 index 000000000..fd52481ce --- /dev/null +++ b/tools/goctl/rpc/test/proto/05_multiple/multi.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +// 场景05:多 service + import,使用 --multiple 模式 +// 预期:goctl --multiple 应同时生成 shared.pb.go 和 multi.pb.go +package multisvc; + +option go_package = "example.com/demo/s05_multiple/pb"; + +import "shared.proto"; + +message SearchReq { + shared.Meta meta = 1; + string keyword = 2; +} + +message SearchReply { + repeated string items = 1; +} + +message NotifyReq { + shared.Meta meta = 1; + string message = 2; +} + +message NotifyReply { + bool ok = 1; +} + +service SearchService { + rpc Search(SearchReq) returns (SearchReply); +} + +service NotifyService { + rpc Notify(NotifyReq) returns (NotifyReply); +} diff --git a/tools/goctl/rpc/test/proto/05_multiple/shared.proto b/tools/goctl/rpc/test/proto/05_multiple/shared.proto new file mode 100644 index 000000000..72680b904 --- /dev/null +++ b/tools/goctl/rpc/test/proto/05_multiple/shared.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +// 场景05:多 service 场景共享的基础类型 +package shared; + +option go_package = "example.com/demo/s05_multiple/pb"; + +message Meta { + string trace_id = 1; + string version = 2; +} diff --git a/tools/goctl/rpc/test/proto/06_wellknown/compare.sh b/tools/goctl/rpc/test/proto/06_wellknown/compare.sh new file mode 100755 index 000000000..2fb301f5d --- /dev/null +++ b/tools/goctl/rpc/test/proto/06_wellknown/compare.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Scenario 06: compare old vs new goctl output — well-known type imports +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Find well-known types include path +PROTOC_INCLUDE="" +PROTOC_BIN="$(which protoc 2>/dev/null || true)" +if [ -n "$PROTOC_BIN" ]; then + CANDIDATE="$(cd "$(dirname "$PROTOC_BIN")/.." && pwd)/include" + [ -f "$CANDIDATE/google/protobuf/timestamp.proto" ] && PROTOC_INCLUDE="$CANDIDATE" +fi +if [ -z "$PROTOC_INCLUDE" ]; then + for d in /opt/homebrew/include /usr/local/include; do + [ -f "$d/google/protobuf/timestamp.proto" ] && PROTOC_INCLUDE="$d" && break + done +fi +if [ -z "$PROTOC_INCLUDE" ]; then + echo "Error: cannot find google/protobuf/timestamp.proto. Install protobuf (brew install protobuf)." + exit 1 +fi +echo "Well-known types: $PROTOC_INCLUDE" + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s06_wellknown > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$OLD_GOCTL" rpc protoc events.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path="$PROTOC_INCLUDE" +verify_build "$OUT_OLD" "old" + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s06_wellknown > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc events.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path="$PROTOC_INCLUDE" +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/06_wellknown/events.proto b/tools/goctl/rpc/test/proto/06_wellknown/events.proto new file mode 100644 index 000000000..ef49dafb0 --- /dev/null +++ b/tools/goctl/rpc/test/proto/06_wellknown/events.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +// 场景06:import google/protobuf/timestamp.proto (well-known type) +// 预期:goctl 正常运行,well-known type 的 pb.go 不会被重复生成(跳过) +package eventsvc; + +option go_package = "example.com/demo/eventsvc"; + +import "google/protobuf/timestamp.proto"; + +message Event { + string id = 1; + string name = 2; + google.protobuf.Timestamp created_at = 3; +} + +message CreateEventReq { + string name = 1; +} + +message CreateEventReply { + Event event = 1; +} + +message ListEventsReq { + int32 page = 1; + int32 size = 2; +} + +message ListEventsReply { + repeated Event events = 1; +} + +service EventService { + rpc CreateEvent(CreateEventReq) returns (CreateEventReply); + rpc ListEvents(ListEventsReq) returns (ListEventsReply); +} diff --git a/tools/goctl/rpc/test/proto/06_wellknown/gen.sh b/tools/goctl/rpc/test/proto/06_wellknown/gen.sh new file mode 100755 index 000000000..896c67326 --- /dev/null +++ b/tools/goctl/rpc/test/proto/06_wellknown/gen.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Scenario 06: well-known type imports +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Find well-known types include path +PROTOC_INCLUDE="" +PROTOC_BIN="$(which protoc 2>/dev/null || true)" +if [ -n "$PROTOC_BIN" ]; then + CANDIDATE="$(cd "$(dirname "$PROTOC_BIN")/.." && pwd)/include" + [ -f "$CANDIDATE/google/protobuf/timestamp.proto" ] && PROTOC_INCLUDE="$CANDIDATE" +fi +if [ -z "$PROTOC_INCLUDE" ]; then + for d in /opt/homebrew/include /usr/local/include; do + [ -f "$d/google/protobuf/timestamp.proto" ] && PROTOC_INCLUDE="$d" && break + done +fi +if [ -z "$PROTOC_INCLUDE" ]; then + echo "Error: cannot find google/protobuf/timestamp.proto. Install protobuf (brew install protobuf)." + exit 1 +fi +echo "Well-known types: $PROTOC_INCLUDE" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s06_wellknown > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc events.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path="$PROTOC_INCLUDE" + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/07_ext_same_pkg/compare.sh b/tools/goctl/rpc/test/proto/07_ext_same_pkg/compare.sh new file mode 100755 index 000000000..0f19af39d --- /dev/null +++ b/tools/goctl/rpc/test/proto/07_ext_same_pkg/compare.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Scenario 07: compare old vs new goctl output — external proto, same Go package +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s07_ext_same_pkg > /dev/null 2>&1) +cd "$SCRIPT_DIR" +set +e +"$OLD_GOCTL" rpc protoc service.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path=./ext_protos +GEN_STATUS=$? +set -e +if [ "$GEN_STATUS" -ne 0 ]; then + echo " ⚠️ old goctl does not support this feature (expected)" +else + verify_build "$OUT_OLD" "old" +fi + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s07_ext_same_pkg > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc service.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path=./ext_protos +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/07_ext_same_pkg/ext_protos/ext.proto b/tools/goctl/rpc/test/proto/07_ext_same_pkg/ext_protos/ext.proto new file mode 100644 index 000000000..b3c589766 --- /dev/null +++ b/tools/goctl/rpc/test/proto/07_ext_same_pkg/ext_protos/ext.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +// 场景07:外部目录的 proto(同 pkg) +// 被 service.proto 通过额外 --proto_path 引入 +// go_package 与 service.proto 相同,最终合并到同一 pb 包 +package ext; + +option go_package = "example.com/demo/s07_ext_same_pkg/pb"; + +message ExtReq { + string key = 1; +} + +message ExtReply { + string value = 1; + int32 code = 2; +} diff --git a/tools/goctl/rpc/test/proto/07_ext_same_pkg/gen.sh b/tools/goctl/rpc/test/proto/07_ext_same_pkg/gen.sh new file mode 100755 index 000000000..9d14d04c3 --- /dev/null +++ b/tools/goctl/rpc/test/proto/07_ext_same_pkg/gen.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Scenario 07: external proto, same Go package +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s07_ext_same_pkg > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc service.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path=./ext_protos + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/07_ext_same_pkg/service.proto b/tools/goctl/rpc/test/proto/07_ext_same_pkg/service.proto new file mode 100644 index 000000000..de0fa6806 --- /dev/null +++ b/tools/goctl/rpc/test/proto/07_ext_same_pkg/service.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +// 场景07:外部 proto(同 Go pkg)—— 请求/响应直接使用 ext.ExtReq / ext.ExtReply +// 两个 proto 的 go_package 相同,最终生成到同一 Go 包(pb),无跨包 import。 +package svc; + +option go_package = "example.com/demo/s07_ext_same_pkg/pb"; + +import "ext.proto"; + +service QueryService { + rpc Query(ext.ExtReq) returns (ext.ExtReply); +} + diff --git a/tools/goctl/rpc/test/proto/08_ext_diff_pkg/compare.sh b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/compare.sh new file mode 100755 index 000000000..fcf0fe78d --- /dev/null +++ b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/compare.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Scenario 08: compare old vs new goctl output — external proto, different Go package +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s08_ext_diff_pkg > /dev/null 2>&1) +cd "$SCRIPT_DIR" +set +e +"$OLD_GOCTL" rpc protoc service.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path=./ext_protos +GEN_STATUS=$? +set -e +if [ "$GEN_STATUS" -ne 0 ]; then + echo " ⚠️ old goctl does not support this feature (expected)" +else + verify_build "$OUT_OLD" "old" +fi + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s08_ext_diff_pkg > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc service.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path=./ext_protos +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/08_ext_diff_pkg/ext_protos/common/types.proto b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/ext_protos/common/types.proto new file mode 100644 index 000000000..9e1070959 --- /dev/null +++ b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/ext_protos/common/types.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +// 场景08:外部目录的 proto(不同 pkg) +// 位于 ext_protos/common/ 子目录,通过 --proto_path=./ext_protos 引入 +// go_package 与主 proto 不同,生成跨包 import +package common; + +option go_package = "example.com/demo/s08_ext_diff_pkg/pb/common"; + +message ExtReq { + string key = 1; + string source = 2; +} + +message ExtReply { + string value = 1; + int32 code = 2; +} diff --git a/tools/goctl/rpc/test/proto/08_ext_diff_pkg/gen.sh b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/gen.sh new file mode 100755 index 000000000..058325e26 --- /dev/null +++ b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/gen.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Scenario 08: external proto, different Go package +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s08_ext_diff_pkg > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc service.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path=./ext_protos + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/08_ext_diff_pkg/service.proto b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/service.proto new file mode 100644 index 000000000..c428d363f --- /dev/null +++ b/tools/goctl/rpc/test/proto/08_ext_diff_pkg/service.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +// 场景08:外部 proto(不同 Go pkg)—— 请求/响应直接使用 common.ExtReq / common.ExtReply +// types.proto go_package 与 service.proto 不同,生成的 Go 代码需要跨包 import。 +package svc; + +option go_package = "example.com/demo/s08_ext_diff_pkg/pb"; + +import "common/types.proto"; + +service DataService { + rpc Fetch(common.ExtReq) returns (common.ExtReply); +} + diff --git a/tools/goctl/rpc/test/proto/09_google_types/compare.sh b/tools/goctl/rpc/test/proto/09_google_types/compare.sh new file mode 100755 index 000000000..db55cf8f1 --- /dev/null +++ b/tools/goctl/rpc/test/proto/09_google_types/compare.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Scenario 09: compare old vs new goctl output — google well-known types as RPC request/response +# Usage: bash compare.sh +# Requires: go install github.com/zeromicro/go-zero/tools/goctl@latest (auto-installed) +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +NEW_GOCTL="$GOCTL_ROOT/bin/goctl" +OLD_GOCTL="$(go env GOPATH)/bin/goctl" +OUT_OLD="$SCRIPT_DIR/output_old" +OUT_NEW="$SCRIPT_DIR/output_new" + +verify_build() { + local dir="$1" label="$2" + echo "Verifying $label ..." + cd "$dir" + go mod tidy + if go build ./...; then + echo " ✅ $label: build passed" + else + echo " ❌ $label: build failed" + exit 1 + fi + cd "$SCRIPT_DIR" +} + +# Find well-known types include path +PROTOC_INCLUDE="" +PROTOC_BIN="$(which protoc 2>/dev/null || true)" +if [ -n "$PROTOC_BIN" ]; then + CANDIDATE="$(cd "$(dirname "$PROTOC_BIN")/.." && pwd)/include" + [ -f "$CANDIDATE/google/protobuf/empty.proto" ] && PROTOC_INCLUDE="$CANDIDATE" +fi +if [ -z "$PROTOC_INCLUDE" ]; then + for d in /opt/homebrew/include /usr/local/include; do + [ -f "$d/google/protobuf/empty.proto" ] && PROTOC_INCLUDE="$d" && break + done +fi +if [ -z "$PROTOC_INCLUDE" ]; then + echo "Error: cannot find google/protobuf/empty.proto. Install protobuf (brew install protobuf)." + exit 1 +fi +echo "Well-known types: $PROTOC_INCLUDE" + +# Install released goctl and build local goctl +echo ">>> Installing goctl@latest ..." +go install github.com/zeromicro/go-zero/tools/goctl@latest +echo ">>> Building local goctl ..." +go build -o "$NEW_GOCTL" "$GOCTL_ROOT" + +# Generate with old goctl +echo ">>> Generating with old goctl ..." +rm -rf "$OUT_OLD" && mkdir -p "$OUT_OLD/pb" +(cd "$OUT_OLD" && go mod init example.com/demo/s09_google_types > /dev/null 2>&1) +cd "$SCRIPT_DIR" +set +e +"$OLD_GOCTL" rpc protoc service.proto \ + --go_out="$OUT_OLD/pb" \ + --go-grpc_out="$OUT_OLD/pb" \ + --zrpc_out="$OUT_OLD/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path="$PROTOC_INCLUDE" +GEN_STATUS=$? +set -e +if [ "$GEN_STATUS" -ne 0 ]; then + echo " ⚠️ old goctl does not support this feature (expected)" +else + verify_build "$OUT_OLD" "old" +fi + +# Generate with new goctl +echo ">>> Generating with new goctl ..." +rm -rf "$OUT_NEW" && mkdir -p "$OUT_NEW/pb" +(cd "$OUT_NEW" && go mod init example.com/demo/s09_google_types > /dev/null 2>&1) +cd "$SCRIPT_DIR" +"$NEW_GOCTL" rpc protoc service.proto \ + --go_out="$OUT_NEW/pb" \ + --go-grpc_out="$OUT_NEW/pb" \ + --zrpc_out="$OUT_NEW/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path="$PROTOC_INCLUDE" +verify_build "$OUT_NEW" "new" + +# Diff old vs new (exclude go.mod / go.sum) +echo "" +echo ">>> Diff (old vs new):" +if diff -rq --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" > /dev/null 2>&1; then + echo " [identical] no differences between old and new output" +else + diff -r --exclude="go.mod" --exclude="go.sum" "$OUT_OLD" "$OUT_NEW" || true +fi diff --git a/tools/goctl/rpc/test/proto/09_google_types/gen.sh b/tools/goctl/rpc/test/proto/09_google_types/gen.sh new file mode 100755 index 000000000..8c0a4bd9d --- /dev/null +++ b/tools/goctl/rpc/test/proto/09_google_types/gen.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Scenario 09: google well-known types as RPC request/response +# Usage: bash gen.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GOCTL_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +GOCTL="$GOCTL_ROOT/bin/goctl" +OUT="$SCRIPT_DIR/output" + +# Find well-known types include path +PROTOC_INCLUDE="" +PROTOC_BIN="$(which protoc 2>/dev/null || true)" +if [ -n "$PROTOC_BIN" ]; then + CANDIDATE="$(cd "$(dirname "$PROTOC_BIN")/.." && pwd)/include" + [ -f "$CANDIDATE/google/protobuf/empty.proto" ] && PROTOC_INCLUDE="$CANDIDATE" +fi +if [ -z "$PROTOC_INCLUDE" ]; then + for d in /opt/homebrew/include /usr/local/include; do + [ -f "$d/google/protobuf/empty.proto" ] && PROTOC_INCLUDE="$d" && break + done +fi +if [ -z "$PROTOC_INCLUDE" ]; then + echo "Error: cannot find google/protobuf/empty.proto. Install protobuf (brew install protobuf)." + exit 1 +fi +echo "Well-known types: $PROTOC_INCLUDE" + +# Build goctl from source +go build -o "$GOCTL" "$GOCTL_ROOT" + +# Clean and initialize output directory +rm -rf "$OUT" && mkdir -p "$OUT/pb" +(cd "$OUT" && go mod init example.com/demo/s09_google_types > /dev/null 2>&1) + +# Generate code +cd "$SCRIPT_DIR" +"$GOCTL" rpc protoc service.proto \ + --go_out="$OUT/pb" \ + --go-grpc_out="$OUT/pb" \ + --zrpc_out="$OUT/rpc" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + --proto_path=. \ + --proto_path="$PROTOC_INCLUDE" + +# Verify build +echo "Running go mod tidy..." +cd "$OUT" && go mod tidy +echo "Checking build..." +if go build ./...; then + echo "✅ Build passed" +else + echo "❌ Build failed" + exit 1 +fi + +echo "Done. Output: $OUT" diff --git a/tools/goctl/rpc/test/proto/09_google_types/service.proto b/tools/goctl/rpc/test/proto/09_google_types/service.proto new file mode 100644 index 000000000..6ff527544 --- /dev/null +++ b/tools/goctl/rpc/test/proto/09_google_types/service.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +// 场景09:请求/响应直接使用 google well-known types +// google.protobuf.Empty 作为返回值(常见的 void 返回模式) +// google.protobuf.Timestamp 直接作为请求/返回类型 +package healthsvc; + +option go_package = "example.com/demo/s09_google_types/pb"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +message HealthCheckRequest { + string service = 1; +} + +service HealthService { + // Ping: 无返回数据,使用 google.protobuf.Empty + rpc Ping(HealthCheckRequest) returns (google.protobuf.Empty); + // GetTime: 直接返回时间戳类型 + rpc GetTime(HealthCheckRequest) returns (google.protobuf.Timestamp); +} +