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 文檔 ),要注意的細節也不少,須要多加實踐,多加總結。

相關文章
相關標籤/搜索