咱們會有這樣的需求:在不一樣的文件夾中定義了不一樣的 proto 文件,這些不一樣的文件夾多是一些不一樣的 gRPC 服務。由於不想重複定義某一個 message
,因此其中一個服務可能會用到其餘服務中定義的 message
,那麼這個時候就須要使用到 proto 文件的 import
功能。git
接下來講說我在 Go 項目中使用 protobuf 的 import
時所遇到的坑。github
首先,咱們來建立一個實驗項目做爲案例,便以說明,結構以下:golang
文件 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 文件中都聲明瞭 package
和 option 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 時是找不到要導入的包的。這裏聽起來可能有點繞,建議你們親自嘗試一下。
另外,咱們說說編譯 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 ago_package
option ofgithub.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
命令時傳入的 --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 文檔 ),要注意的細節也不少,須要多加實踐,多加總結。