业务场景

我们会有这样的需求:在不同的文件夹中定义了不同的 proto 文件,这些不同的文件夹可能是一些不同的 gRPC 服务。因为不想重复定义某一个 message,所以其中一个服务可能会用到其他服务中定义的 message,那么这个时候就需要使用到 proto 文件的 import 功能。

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

案例

首先,我们来创建一个实验项目作为案例,便以说明,结构如下:

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

一般情况下,我们会将编译生成的 pb.go 文件生成在与 proto 文件相同的目录,这样我们就不需要再创建相同的目录层级结构来存放 pb.go 文件了。由于同一文件夹下的 pb.go 文件同属于一个 package,所以在定义 proto 文件的时候,相同文件夹下的 proto 文件也应声明为同一的 package,并且和文件夹同名,这是因为生成的 pb.go 文件的 package 是取自 proto package 的。

同属于一个包内的 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:

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 文件中都声明了 package 和 option go_package,这两个声明都是包声明,到底两者有什么关系,这也是我开始比较迷惑的。

我是这样理解的,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 文件

其中,--proto_path 或者 -I 参数用以指定所编译源码(包括直接编译的和被导入的 proto 文件)的搜索路径,proto 文件中使用 import 关键字导入的路径一定是要基于 --proto_path 参数所指定的路径的。该参数如果不指定,默认为 pwd ,也可以指定多个以包含所有所需文件。

其中,--go_out 参数是用来指定 protoc-gen-go 插件的工作方式 和 go 代码目录架构的生成位置,可以向 --go_out 传递很多参数,见 golang/protobuf 文档 。主要的两个参数为 plugins 和 paths ,代表 生成 go 代码所使用的插件 和 生成的 go 代码的目录怎样架构。--go_out 参数的写法是,参数之间用逗号隔开,最后加上冒号来指定代码目录架构的生成位置,例如:--go_out=plugins=grpc,paths=import:. 。paths 参数有两个选项,import 和 source_relative 。默认为 import ,代表按照生成的 go 代码的包的全路径去创建目录层级,source_relative 代表按照 proto 源文件的目录层级去创建 go 代码的目录层级,如果目录已存在则不用创建

在上面的示例命令中,--go_out 默认使用了 paths=import 所以,我的 go 文件都被编译到了 ./github.com/xvrzhao/pb-demo/proto/user/ 下,后来阅读 文档 才发现:

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 路径写为相对于项目根目录的路径,然后 protoc 的执行总是在项目根目录下进行:

$ 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 文档 ),要注意的细节也很多,需要多加实践,多加总结。

转载地址:Protobuf 的 import 功能在 Go 项目中的实践 - Go语言中文网 - Golang中文社区

Protobuf 的 import 功能在 Go 项目中的实践相关推荐

  1. 使用zinnia制作android手写输入功能(下)-------------------在项目中使用zinnia

    使用zinnia制作android手写输入功能(下)-------------------在项目中使用zinnia

  2. Effective Dart 文档注释在Flutter项目中的实践

    前言 什么是注释? 在编程语言中,注释就是对代码的解释和说明,其目的是让人们能够更加轻松地了解代码. 也有一句话是这样说的:程序员都讨厌两件事,1.别人不写注释 2.自己写注释 在开发者社区里,我不止 ...

  3. 数加平台在数据挖掘项目中的实践

    目录 背景 数据源 数据处理 模型训练及预测 数据可视化 总结 1. 背景 返回目录 随着越来越多的公司开始重视数据的积累和产品的敏捷开发,数据量的不断增加和开发周期的不断缩短,就形成了一对明显的矛盾 ...

  4. 数加平台在数据挖掘项目中的实践 1

    目录 背景 数据源 数据处理 模型训练及预测 数据可视化 总结 1. 背景 返回目录 随着越来越多的公司开始重视数据的积累和产品的敏捷开发,数据量的不断增加和开发周期的不断缩短,就形成了一对明显的矛盾 ...

  5. vue-ssr在项目中的实践

    写在文前 由于前端脚手架.打包工具.Node等版本的多样性,本文无法同时兼顾,文中所述皆基于以下技术栈进行. 脚手架:vue-cli3 打包工具:webpack4,集成在vue-cli3中,通过修改v ...

  6. 流程图布局在项目中的实践

    背景 随着项目中流程图被运营同学画的越来越复杂,出现了一个流程图上存在几百个节点的情况,流程图带来的业务逻辑可视化效果越来越不明显.虽然我们提供了分组.注释.记号等修饰性功能去支持标识流程中重要的业务 ...

  7. methods vue过滤器 和_数据动态过滤技巧在 Vue 项目中的实践

    这个问题是在下在做一个 Vue 项目中遇到的实际场景,这里记录一下我遇到问题之后的思考和最后怎么解决的(老年程序员记性不好 -.-),过程中会涉及到一些Vue源码的概念比如 $mount. rende ...

  8. 网关 Apache APISIX 在 360 基础运维平台项目中的实践

    女主宣言 今天小编为大家分享一篇关于Apache APISIX的文章,文章从开发者的角度讲述了 Apache APISIX 网关在 360 基础运维平台的落地实践,希望能对大家有所帮助. PS:丰富的 ...

  9. ElasticSearch 在 Spring 项目中的实践

    前言 ElasticSearch简称es,是一个开源的高扩展的分布式全文检索引擎. 它可以近乎实时的存储.检索数据,其扩展性很好,ElasticSearch是企业级应用中较为常见的技术. 下面和大家分 ...

最新文章

  1. linux环境安装python-pip
  2. C#创建一个Windows Service
  3. Hermite曲线与Bezier曲线的关系
  4. tableau linux无网络安装_举个栗子!Tableau 技巧(110)两种方法实现正态分布 Normal distribution...
  5. 32位x86处理器编程导入——《x86汇编语言:从实模式到保护模式》读书笔记08
  6. 9.为包含指针的关联容器指定比较类型
  7. java程序设计自考_java程序设计自考试题
  8. 弃用数据库自增ID,曝光一下我自己用到的解决方法之---终结篇
  9. hdu4734 F(x)
  10. C语言指针类型和类型转换
  11. C语言实现EEPROM程序读取24C02成功读取方案单片机用
  12. 普通链接二维码打开小程序实现动态传递参数
  13. 软件Craft.io指导
  14. 梅特勒托利多xk3124电子秤说明书_托利多电子秤详细说明书
  15. 金融证券基础(三)----回撤
  16. HUST Trainning 2015-06-14
  17. QQ机器人——基于python以及qqbot模块
  18. 厦门大学2018年转专业计算机,厦门大学各学院2017年接收转专业学生计划.PDF
  19. google翻译不能用后chrome浏览器如何翻译网页
  20. IDEA 断点调试,为啥断点没有起作用

热门文章

  1. Mac IDEA启动SpringBoot项目过慢
  2. 计算机控制考试试卷及答案,计算机控制系统A考试试卷带答案.doc
  3. 巧用 PHP 数组函数 1
  4. AutoIt Windows的自动化脚本
  5. PHP生成随机密码的4种方法及性能对比
  6. 用 普通 用户欺骗登陆 获取 管理员的最高权限~~~~
  7. LeetCode:Combinations
  8. Android4.4 及以下TextView,Button等控件使用矢量图报错
  9. 工作五年的UI设计师,现在混的怎么样?不看是你的损失
  10. UVA 11992 - Fast Matrix Operations(段树)