Protobuf 的 import 功能在 Go 项目中的实践

业务场景

咱们会有这样的需求:在不一样的文件夹中定义了不一样的 proto 文件,这些不一样的文件夹多是一些不一样的 gRPC 服务。由于不想重复定义某一个 message,因此其中一个服务可能会用到其余服务中定义的 message,那么这个时候就须要使用到 proto 文件的 import 功能。git

接下来讲说我在 Go 项目中使用 protobuf 的 import 时所遇到的坑。github

案例

首先,咱们来建立一个实验项目做为案例,便以说明,结构以下:golang

image.png

文件 go.mod 中声明了该项目模块名 module github.com/xvrzhao/pb-demo,proto 文件夹中含有两个 gRPC 服务,分别为 article 和 user,咱们在这两个文件夹中定义各自所须要的 messages 和 services。ui

通常状况下,咱们会将编译生成的 pb.go 文件生成在与 proto 文件相同的目录,这样咱们就不须要再建立相同的目录层级结构来存放 pb.go 文件了。因为同一文件夹下的 pb.go 文件同属于一个 package,因此在定义 proto 文件的时候,相同文件夹下的 proto 文件也应声明为同一的 package,而且和文件夹同名,这是由于生成的 pb.go 文件的 package 是取自 proto package 的。spa

同属于一个包内的 proto 文件之间的引用也须要声明 import ,由于每一个 proto 文件都是相互独立的,这点不像 Go(包内全部定义都可见)。咱们的项目 user 模块下 service.proto 就须要用到 message.proto 中的 message 定义,代码是这样写的:插件

user/service.proto:命令行

syntax = "proto3";  
package user;  // 声明所在包
option go_package = "github.com/xvrzhao/pb-demo/proto/user";  // 声明生成的 go 文件所属的包
  
import "proto/user/message.proto";  // 导入同包内的其余 proto 文件
import "proto/article/message.proto";  // 导入其余包的 proto 文件
  
service User {  
    rpc GetUserInfo (UserID) returns (UserInfo);  
    rpc GetUserFavArticle (UserID) returns (article.Articles.Article);  
}

user/message.proto:code

syntax = "proto3";  
package user;  
option go_package = "github.com/xvrzhao/pb-demo/proto/user";  
  
message UserID {  
    int64 ID = 1;  
}  
  
message UserInfo {  
    int64 ID = 1;  
    string Name = 2;  
    int32 Age = 3;  
    gender Gender = 4;  
    enum gender {  
        MALE = 0;  
        FEMALE = 1;  
    }  
}

能够看到,咱们在每一个 proto 文件中都声明了 packageoption go_package,这两个声明都是包声明,到底二者有什么关系,这也是我开始比较迷惑的。blog

我是这样理解的,package 属于 proto 文件自身的范围定义,与生成的 go 代码无关,它不知道 go 代码的存在(但 go 代码的 package 名每每会取自它)。这个 proto 的 package 的存在是为了不当导入其余 proto 文件时致使的文件内的命名冲突。因此,当导入非本包的 message 时,须要加 package 前缀,如 service.proto 文件中引用的 Article.Articles,点号选择符前为 package,后为 message。同包内的引用不须要加包名前缀递归

article/message.proto:

syntax = "proto3";  
package article;  
option go_package = "github.com/xvrzhao/pb-demo/proto/article";  
  
message Articles {  
    repeated Article Articles = 1;  
    message Article {  
        int64 ID = 1;  
        string Title = 2;  
    }  
}

option go_package 的声明就和生成的 go 代码相关了,它定义了生成的 go 文件所属包的完整包名,所谓完整,是指相对于该项目的完整的包路径,应以项目的 Module Name 为前缀。若是不声明这一项会怎么样?最开始我是没有加这项声明的,后来发现 依赖这个文件的 其余包的 proto 文件 所生成的 go 代码 中(注意断句,已用斜体和正体标示),引入本文件所生成的 go 包时,import 的路径并非基于项目 Module 的完整路径,而是在执行 protoc 命令时相对于 --proto_path 的包路径,这在 go build 时是找不到要导入的包的。这里听起来可能有点绕,建议你们亲自尝试一下。

protoc 命令

另外,咱们说说编译 proto 文件时的命令参数。

首先 protoc 编译生成 go 代码所用的插件 protoc-gen-go 是不支持多包同时编译的,执行一次命令只能同时编译一个包,关于该讨论能够查看该项目的 issue#39

接下来说讲我遇到的另一个坑。一般状况下咱们编译命令是这样的(基于本项目,pwd 为项目根目录):

$ protoc --proto_path=. --go_out=. ./proto/user/*.proto # 编译 user 路径下全部 proto 文件

--go_out 参数指定了生成的 go 文件路径为 . ,在没有声明 option go_package 时,该路径为相对 proto 文件的路径,也就是让 go 文件生成到和 proto 文件相同的文件夹。可是,我声明了 option go_package 后发现 go 文件编译到了 ./github.com/xvrzhao/pb-demo/proto/user/ 下,这是编译器自动建立的路径。

后来阅读 protoc-gen-go 的 README 才发现:

However, the output directory is selected in one of two ways. Let us say we have inputs/x.proto with a go_package option of github.com/golang/protobuf/p . The corresponding output file may be:

  • Relative to the import path:
$ protoc --go_out=. inputs/x.proto
# writes ./github.com/golang/protobuf/p/x.pb.go

( This can work well with --go_out=$GOPATH )

  • Relative to the input file:
$ protoc --go_out=paths=source_relative:. inputs/x.proto
# generate ./inputs/x.pb.go

因此,咱们应该将 --go_out 参数改成 --go_out=paths=source_relative:.

请切记 option go_package 声明和 --go_out=paths=source_relative:. 命令行参数缺一不可

  • option go_package 声明 是为了让生成的其余 go 包(依赖方)能够正确 import 到本包(被依赖方)
  • --go_out=paths=source_relative:. 参数 是为了让加了 option go_package 声明的 proto 文件能够将 go 代码编译到与其同目录。

proto 文件的 import 路径

另外再说一下在 proto 文件中导入其余文件时,import 后跟的路径是相对于谁的问题。

这个路径是相对于执行 protoc 命令时传入的 --proto_path 参数的,这个参数表明搜索 被 import 的文件 的路径,能够指定多个,也能够不指定。不指定的话,默认搜索路径为 pwd,指定的话为搜索路径为全部指定的路径 + pwd,因此 proto 文件中 import 路径应该声明为 --proto_path 下的路径

为了统一性,我会将全部 import 路径写为相对于项目根目录的路径,而后 protoc 的执行老是在项目根目录下进行,如:

pb-demo 下执行:

$ protoc --go_out=plugins=grpc,paths=source_relative:. ./proto/user/*.proto 
$ protoc --go_out=plugins=grpc,paths=source_relative:. ./proto/article/*.proto

若是你以为每一个包都须要单独编译,有些麻烦,能够执行脚本( **/* 表明递归获取当前目录下全部的文件和文件夹):

pb-demo 下执行:

$ for x in **/*.proto; do protoc --go_out=plugins=grpc,paths=source_relative:. $x; done

循环依赖

注意,不一样包之间的 proto 文件不能够循环依赖,这会致使生成的 go 包之间也存在循环依赖,致使 go 代码编译不经过。

总结

感受 protobuf 的使用,很是的繁杂,文档散落在各处( protobuf 官方文档 / golang protobuf 文档 / grpc 文档 ),要注意的细节也不少,须要多加实践,多加总结。

相关文章
相关标签/搜索