gRPC-Gateway 实战:使用 protoc 从 .proto 文件生成 Go 类型与 gRPC Stub 代码
【免费下载链接】grpc-gatewaygRPC to JSON proxy generator following the gRPC HTTP spec项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-gateway
本教程是 gRPC-Gateway 官方文档中「Generating stubs」系列的一部分,围绕如何在仓库根目录下、以protoc命令行工具为入口,从proto/helloworld/hello_world.proto生成 Go 所需的全部桩代码(stub):包括消息类型代码*.pb.go与 gRPC 服务定义代码*_grpc.pb.go。读完本文,你将掌握protoc各核心参数(-I、--go_out、--go-grpc_out、paths=source_relative等)的确切含义与用法,知道生成产物落在何处、每个文件承担什么职责,并能顺藤摸瓜扩展出 gRPC-Gateway 的 HTTP 反向代理代码(*.gw.pb.go)。
生成 Go Stub 的两条路线:protoc 与 buf
在开始动手之前,先明确本教程在整个 gRPC-Gateway 教程体系中的位置。官方文档在 Generating stubs 一节中明确说明:生成 stub 有两条可选路线——protoc与buf。
protoc:Protocol Buffers 官方编译器,是业界使用最广泛的经典生成方式,但也存在较陡峭的学习曲线(所有.proto文件都需要在命令行上手工指定,各类*_out、*_opt参数繁多)。buf:较新的工具,以用户体验和速度为设计目标,额外提供 lint(代码风格检查)与 breaking change detection(破坏性变更检测),这是protoc不具备的能力。
本文聚焦protoc路线;想了解 buf 方案的读者可阅读姊妹篇 使用 buf 生成 stubs。值得提前说明的是,gRPC-Gateway 仓库自身在 Makefile 的proto目标中统一使用buf generate驱动代码生成,并以 buf.gen.yaml 作为模板;但理解protoc的参数体系仍然是理解这一切生成机制的基础。
前置准备:protoc 与两个 Go 插件
protoc本身只负责解析.proto文件,具体产物的生成由「插件(plugin)」完成。插件以protoc-gen-<name>命名,protoc会在PATH中查找它们。生成 Go 桩代码需要两个官方插件:
| 插件 | 生成内容 | 典型产物 |
|---|---|---|
protoc-gen-go(命令行参数--go_out) | Go 消息类型(message编译成的 Go struct 及其序列化逻辑) | *.pb.go |
protoc-gen-go-grpc(命令行参数--go-grpc_out) | gRPC 服务定义:客户端接口、服务端接口、注册函数、UnimplementedXxxServer骨架 | *_grpc.pb.go |
安装这两个插件的方式是go install(具体版本可参考仓库 go.mod 中锁定的google.golang.org/protobuf与google.golang.org/grpc版本,安装时选择与之兼容的插件版本即可):
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest同时确保本机已安装protoc本体。如果你还需要生成 gRPC-Gateway 的 HTTP 代理代码,还需安装仓库自带的protoc-gen-grpc-gateway(见下文「扩展:生成 gRPC-Gateway 反向代理代码」一节)。
编写一个可供生成的 .proto 文件
在运行protoc之前,需要先有一个.proto文件。官方教程 创建一个简单的 Hello World gRPC 服务 给出了最小示例:在proto/helloworld/hello_world.proto中定义一个Greeter服务及其SayHelloRPC,以及HelloRequest/HelloReply两个消息。
syntax = "proto3"; package helloworld; // The greeting service definition service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }仓库中的真实示例 examples/internal/helloworld/helloworld.proto 更完整地展示了实际工程中会出现的两个关键点:
go_package选项:通过option go_package = "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/helloworld";声明生成的 Go 文件所属的 import 路径。protoc-gen-go生成代码时依赖它确定package声明与 import 路径,这是 Go 工程中每个.proto文件都应当配置的选项。google.api.http注解与google.protobuf.wrappers等依赖:import "google/api/annotations.proto";是后续让 gRPC-Gateway 生成 HTTP 映射代码的前提;一旦 proto 文件引入了外部依赖,protoc的-I参数就必须覆盖到这些依赖所在的目录(详见下文)。
核心命令:protoc 生成 Go stub 的完整示例
官方文档给出的protoc命令如下,假设你正位于仓库根目录,且.proto文件都放在名为proto的目录下:
$ protoc -I ./proto \ --go_out ./proto --go_opt paths=source_relative \ --go-grpc_out ./proto --go-grpc_opt paths=source_relative \ ./proto/helloworld/hello_world.proto这一条命令同时完成两件事:用go插件生成 Go 消息类型,用go-grpc插件生成 gRPC 服务定义。下面逐段拆解每个参数的实际作用。
-I ./proto:设置 import 搜索路径
-I(等价于--proto_path)告诉protoc到哪些目录下查找.proto文件及其 import 的依赖。它有两个作用:
- 确定命令行末尾列出的
.proto文件的解析入口; - 为
.proto文件内部的import语句提供搜索根。例如当你的 proto 引入google/api/annotations.proto时,protoc会沿着-I指定的路径去找它。
官方文档在 adding_annotations.md 中演示了完整的目录布局:将googleapis中的annotations.proto与http.proto复制进本地 proto 结构后,目录会呈现如下形态:
proto ├── google │ └── api │ ├── annotations.proto │ └── http.proto └── helloworld └── hello_world.proto此时只要-I ./proto,protoc就能解析import "google/api/annotations.proto"。
--go_out与--go_opt paths=source_relative
--go_out ./proto指定protoc-gen-go的输出目录。输出目录决定的是「顶层根目录」,而生成文件具体落在哪个子路径,则由paths模式决定——这正是--go_opt paths=source_relative的作用。
protoc-gen-go支持两种paths取值:
import(默认模式):按go_package声明的 import 路径逐级创建目录,并把文件写入其中。例如go_package = "github.com/myuser/myrepo/proto/helloworld"时,产物会出现在以输出目录为根、逐级嵌套的github.com/myuser/myrepo/proto/helloworld/下。这种模式适合生成代码与源码隔离放置的场景。source_relative:忽略go_package的路径信息,直接把生成文件写到与源.proto文件相同的目录下。这就是官方文档强调的「generated files will appear in the same directory as the source.protofile」。
官方文档推荐的source_relative模式对初学者更直观:运行命令后,proto/helloworld/下会同时出现源文件和生成文件,对照检查非常方便。
--go-grpc_out与--go-grpc_opt paths=source_relative
go-grpc插件(protoc-gen-go-grpc)的用法与go插件完全对称:--go-grpc_out指定输出目录,--go-grpc_opt paths=source_relative同样让生成文件落在源 proto 同目录。它将service Greeter { rpc SayHello(...) }编译为:
- 客户端接口
GreeterClient及其默认实现greeterClient; - 服务端接口
GreeterServer与安全骨架UnimplementedGreeterServer(方便你嵌入自己的实现而无需实现全部方法); - 注册函数
RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer)。
官方教程 创建 main.go 中server结构体嵌入helloworldpb.UnimplementedGreeterServer、再调用helloworldpb.RegisterGreeterServer(s, &server{})的写法,依赖的正是go-grpc插件生成的这些类型与函数。
生成产物:*.pb.go与*_grpc.pb.go
命令运行完毕后,proto/helloworld/hello_world.proto会对应生成两个文件:
proto/helloworld/ ├── hello_world.proto # 源文件 ├── hello_world.pb.go # 由 go 插件生成:HelloRequest / HelloReply 等消息类型 └── hello_world_grpc.pb.go # 由 go-grpc 插件生成:Greeter 服务的客户端与服务端定义这一「一个源文件、两个产物」的规律可以直接在仓库中得到验证。仓库中examples/internal/helloworld/目录存放了真实生成好的三件套:
- examples/internal/helloworld/helloworld.proto:源文件,含
google.api.http注解; - examples/internal/helloworld/helloworld.pb.go:
HelloRequest/HelloReply消息类型代码; - examples/internal/helloworld/helloworld_grpc.pb.go:
Greeter服务定义代码。
另外,仓库中该目录还有helloworld.pb.gw.go与helloworld.swagger.json、helloworld.openapi.json,它们分别由 gRPC-Gateway 与 OpenAPI 生成器产出(见下一节),正好构成了一个完整示例应包含的全部生成物。
扩展:追加 grpc-gateway 插件,生成 HTTP 反向代理代码
只生成 Go 类型与 gRPC 服务定义还不足以让 gRPC-Gateway 工作。要让网关把 HTTP/JSON 请求转发到 gRPC 服务,还需要protoc-gen-grpc-gateway插件生成*.gw.pb.go文件。官方教程 adding_annotations.md 给出了在protoc命令中追加该插件的完整写法:
$ protoc -I ./proto \ --go_out ./proto --go_opt paths=source_relative \ --go-grpc_out ./proto --go-grpc_opt paths=source_relative \ --grpc-gateway_out ./proto --grpc-gateway_opt paths=source_relative \ ./proto/helloworld/hello_world.proto前提有两个:
proto 文件中的 RPC 方法带有
google.api.http注解,例如:import "google/api/annotations.proto"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { post: "/v1/example/echo" body: "*" }; } }本地 proto 结构中包含
google/api/annotations.proto与google/api/http.proto(即上文的目录布局),且-I能搜索到它们。
运行后,protoc-gen-grpc-gateway会额外生成*.gw.pb.go。仓库中 examples/internal/helloworld/helloworld.pb.gw.go 即是此类产物的实例,其中包含RegisterGreeterHandler等函数,供 runtime.NewServeMux() 注册使用——这正是 adding_annotations.md 中main.go示例里helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn)一行背后的代码来源。protoc-gen-grpc-gateway的完整实现位于 protoc-gen-grpc-gateway/internal/gengateway,对生成逻辑感兴趣的读者可以深入阅读其 generator.go。
仓库实践佐证:从 Makefile 与 buf 配置反推 protoc 参数语义
虽然 gRPC-Gateway 仓库自身使用 buf 驱动生成(见 buf.gen.yaml,其中以paths=source_relative为go、go-grpc、grpc-gateway、openapiv2四个插件统一配置输出模式),但 Makefile 的proto目标可以反证本教程讲解的参数体系在实际工程中的对应关系:
- 生成 Go 类型与服务定义:
buf generate使用buf.build/protocolbuffers/go与buf.build/grpc/go两个插件,等价于protoc的--go_out与--go-grpc_out; - 生成网关代码:
local: protoc-gen-grpc-gateway插件,等价于protoc的--grpc-gateway_out; - 生成 OpenAPI 文档:
local: protoc-gen-openapiv2插件,产出*.swagger.json(仓库中如 examples/internal/proto/examplepb/a_bit_of_everything.swagger.json); - 全局的
paths=source_relative选项与protoc路线完全一致,说明无论走哪条路线,推荐的输出布局是统一的。
仓库根目录 go.mod 的模块名为github.com/grpc-ecosystem/grpc-gateway/v2,这解释了 examples/internal/helloworld/helloworld.proto 中go_package为何以该路径开头——go_package必须与最终放置生成文件的 Go module 内的路径保持一致,才能被正常 import。
常见问题与注意事项
- 生成的包无法 import?检查两处:
go_package选项是否与文件在 module 内的实际路径一致;生成文件是否真的落在go_package对应目录(若使用默认paths=import模式,文件会按go_package嵌套输出,务必确认 import 路径与磁盘路径匹配)。 --go_opt与--go-grpc_opt必须分别指定:两个插件各自独立维护paths等选项,只写--go_opt paths=source_relative不会作用于go-grpc插件。- proto 引入了外部依赖却解析失败?将依赖文件放入
-I覆盖的某个目录(如上文proto/google/api/布局),并确认命令行末尾仅列出待编译的源文件、而非依赖文件。 - 想统一管理生成流程?当 proto 文件数量增多后,逐一手写
protoc命令会变得冗长易错,这正是官方推荐迁移到 buf(使用 buf 生成 stubs)的原因;buf 会递归发现配置目录下的所有.proto文件并自动构建,省去手工罗列。
下一步
生成完*.pb.go与*_grpc.pb.go之后,教程的下一个环节是编写 main.go 启动 gRPC 服务,将Greeter注册到grpc.NewServer()上运行;随后在 adding_annotations.md 中为 proto 添加google.api.http注解并生成*.gw.pb.go,最终在同一个进程中同时监听 gRPC 端口(:8080)与 HTTP 网关端口(:8090),用curl验证 JSON 请求到 gRPC 调用的完整链路。
【免费下载链接】grpc-gatewaygRPC to JSON proxy generator following the gRPC HTTP spec项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-gateway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考