Protobuf 的 import 功能在 Go 項目中的實踐


業務場景

我們會有這樣的需求:在不同的文件夾中定義了不同的 proto 文件,這些不同的文件夾可能是一些不同的 gRPC 服務。因為不想重復定義某一個 message,所以其中一個服務可能會用到其他服務中定義的 message,那么這個時候就需要使用到 proto 文件的 import 功能。

接下來說說我在 Go 項目中使用 protobuf 的 import 時所遇到的坑。

案例

首先,我們來創建一個實驗項目作為案例,便以說明,結構如下:

image.png

文件 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 in **/*.proto; do protoc --go_out=plugins=grpc,paths=source_relative:. $x; done

循環依賴

注意,不同包之間的 proto 文件不可以循環依賴,這會導致生成的 go 包之間也存在循環依賴,導致 go 代碼編譯不通過。

總結

感覺 protobuf 的使用非常繁雜,文檔散落在各處( protobuf 官方文檔 / golang protobuf 文檔 / grpc 文檔 ),要注意的細節也很多,需要多加實踐,多加總結。

 

https://segmentfault.com/a/1190000021456180

https://www.jianshu.com/p/a0286b58098e


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM