[譯] Go 釋出新版 Protobuf API

司徒公子發表於2020-04-01

Go 釋出新版 Protobuf API

介紹

我們很高興地宣佈:釋出protocol buffers的 Go API 主要修訂版本 —— Google 獨立於程式語言的資料交換介面格式。

構建新 API 的動機

第一個用於 Go 的 protocol buffer 版本由 Rob Pike 在 2010 年 3 月釋出,Go 的首個正式版在兩年後才釋出。

在第一個版本釋出的數十年間,隨著 Go 的發展,package 也在不斷髮展壯大。使用者的需求也在不斷的增長。

許多人希望使用 reflection(反射) package 來編寫檢查 protocol buffer message 的程式,reflect package 提供了 Go 型別和值的檢視,但是忽略了 protocol buffer 型別系統的資訊。例如,我們可能希望編寫一個函式來遍歷日誌項,清除所有標註為敏感資訊的資料,標註並不是 Go 型別系統的一部分。

另一個常見的需求就是使用 protocol buffer 編譯器來生成其他的資料結構,例如動態 message 型別,它能夠表示在編譯時型別未知的 message。

我們還觀察到,時常發生問題的根源在於 proto.Message 介面,該介面標識生成的 message 型別的值,對描述這些型別的行為幾乎沒有任何幫助。當使用者建立實現該介面的型別(時常不經意間將 message 嵌入其他的結構中),並且將這些型別的值傳遞給期待生成 message 值的函式時,程式發生崩潰或行為難以預料。

這三個問題都有一個共同的原因,而通常的解決方法:Message 介面應該完全指定 message 的行為,對 Message 值進行操作的函式應該自由的接收任何型別,這些型別的介面都要被正確的實現。

由於不可能在保持 package API 相容性的同時更改 Message 型別的現有定義,所以我們決定是時候開始開發新的、不相容 protobuf 模組的主要版本了。

今天,我們很高興地釋出這個新模組,希望你們喜歡。

Reflection(反射)

Reflection(反射)是新實現的旗艦特性。與 reflect 包提供 Go 型別和值的檢視相似,protoreflect 包根據 protocol buffer 型別系統提供值的檢視。

完整的描述 protoreflect package 對於這篇文章來說太長了,但是,我們可以來看看如何編寫前面提到的日誌清理函式。

首先,我們將編寫 .proto 檔案來定義 google.protobuf.FieldOptions 型別的副檔名,以便我們可以將註釋欄位作為標識敏感資訊的與否。

syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
    bool non_sensitive = 50000;
}
複製程式碼

我們可以使用此選項來將某些欄位標識為非敏感欄位。

message MyMessage {
    string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}
複製程式碼

接下來,我們將編寫一個 Go 函式,它用於接收任意 message 值以及刪除所有敏感欄位。

// 清除 pb 中所有的敏感欄位
func Redact(pb proto.Message) {
   // ...
}
複製程式碼

函式接收 proto.Message 引數,這是由所有已生成的 message 型別實現的介面型別。此型別是 protoreflect 包中已定義的別名:

type ProtoMessage interface{
    ProtoReflect() Message
}
複製程式碼

為了避免填充生成 message 的名稱空間,介面僅包含一個返回 protoreflect.Message 的方法,此方法提供對 message 內容的訪問。

(為什麼是別名?由於 protoreflect.Message 有返回原始 proto.Message 的相應方法,我們需要避免在兩個包中迴圈匯入。)

protoreflect.Message.Range 方法為 message 中的每一個填充欄位呼叫一個函式。

m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
    // ...
    return true
})
複製程式碼

使用描述 protocol buffer 型別的 protoreflect.FieldDescriptor 欄位和包含欄位值的 protoreflect.Value 欄位來呼叫 range 函式。

protoreflect.FieldDescriptor.Options 方法以 google.protobuf.FieldOptions message 的形式返回欄位選項。

opts := fd.Options().(*descriptorpb.FieldOptions)
複製程式碼

(為什麼使用型別斷言?由於生成的 descriptorpb package 依賴於 protoreflect,所以 protoreflect package 無法返回正確的選項型別,否則會導致迴圈匯入的問題)

然後,我們可以檢查選項以檢視擴充套件為 boolean 型別的值:

if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
    return true // 不要刪減非敏感欄位
}
複製程式碼

請注意,我們在這裡看到的是欄位描述符,而不是欄位,我們感興趣的資訊在於 protocol buffer 型別系統,而不是 Go 語言。

這也是我們已經簡化了 proto package API 的一個示例,原來的 proto.GetExtension 返回一個值和錯誤資訊,新的 proto.GetExtension 只返回一個值,如果欄位不存在,則返回該欄位的預設值。在 Unmarshal 的時候報告擴充套件解碼錯誤。

一旦我們確定了需要修改的欄位,將其清除就很簡單了:

m.Clear(fd)
複製程式碼

綜上所述,我們完整的修改函式如下:

// 清除 pb 中的所有敏感欄位
func Redact(pb proto.Message) {
    m := pb.ProtoReflect()
    m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
        opts := fd.Options().(*descriptorpb.FieldOptions)
        if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
            return true
        }
        m.Clear(fd)
        return true
    })
}
複製程式碼

一個更加完整的實現應該是以遞迴的方式深入這些 message 值欄位。我們希望這些簡單的示例能讓你更瞭解 protocol buffer reflection(反射)以及它的用法。

版本

我們將 Go protocol buffer 的原始版本稱為 APIv1,新版本稱為 APIv2。因為 APIv2 不支援向前相容 APIv1,所以我們需要為每個模組使用不同的路徑。

(這些 API 版本與 protocol buffer 語言的版本:proto1proto2proto3 是不同的,APIv1 和 APIv2 是 Go 中的具體實現,他們都支援 proto2proto3 語言版本。)

github.com/golang/protobuf 模組是 APIv1。

google.golang.org/protobuf 模組是 APIv2。我們利用需要改變匯入路徑來切換版本,將其繫結到不同的主機提供商上。(我們考慮了 google.golang.org/protobuf/v2,說得更清楚一點,這是 API 的第二個主要版本,但是從長遠來看,我們認為更短的路徑名是更好的選擇。)

我們知道不是所有的使用者都以相同的速度遷移到新的 package 版本中,有些會迅速遷移,其他的可能會無限期的停留在老版本上。甚至在一個程式中,也有可能使用不同的 API 版本,這是至關重要的。所以,我們繼續支援使用 APIv1 的程式。

  • github.com/golang/protobuf@v1.3.4 是 APIv1 最新 pre-APIv2 版本。
  • github.com/golang/protobuf@v1.4.0 是由 APIv2 實現的 APIv1 的一個版本。API 是相同的,但是底層實現得到了新 API 的支援。該版本包含 APIv1 和 APIv2 之間的轉換函式,proto.Message 介面來簡化兩者之間的轉換。
  • google.golang.org/protobuf@v1.20.0 是 APIv2,該模組取決於 github.com/golang/protobuf@v1.4.0,所以任何使用 APIv2 的程式都將會自動選擇一個與之對應的整合 APIv1 的版本。

(為什麼要從 v1.20.0 版本開始?為了清晰的提供服務,我們預計 APIv1 不會達到 v1.20.0。因此,版本號就足以區分 APIv1 和 APIv2。)

我們打算長期地保持對 APIv1 的支援。

無論使用哪個 API 版本,該組織都會確保任何給定的程式都僅使用單個 protocol buffer 來實現。它允許程式逐步採用新的 API 或者完全不採用,同時仍然獲得新實現的優勢。最低版本選擇原則意味著程式需要保留原來的實現方法,直到維護者選擇更新到新的版本(直接升級或通過更新依賴項)。

注意其他的一些特性

google.golang.org/protobuf/encoding/protojson package 使用規範 JSON 對映將 protocol buffer message 轉化為 JSON,並修復了舊 jsonpb package 的一些問題,這些問題很難在不影響現有使用者的情況下進行更改。

google.golang.org/protobuf/types/dynamicpb package 提供了對 message 中 proto.Message 的實現,用於在執行時派生 protocol buffer 型別的 message。

google.golang.org/protobuf/testing/protocmp package 提供了使用 github.com/google/cmp package 來比較 protocol buffer message 的函式。

google.golang.org/protobuf/compiler/protogen package 提供了對編寫 protocol 編譯器外掛的支援。

結論

google.golang.org/protobuf 模組是對 Go protocol buffer 支援的重大改進,為反射(reflection)、自定義 message 實現以及整潔的 API surface 提供優先的支援。我們打算用新的 API 包裝的方式來永久維護原來的 API,從而使得使用者可以按照自己的節奏逐步採用新的 API。

我們這次更新的目標是在解決舊 API 問題的同時,放大舊 API 的優勢。當我們完成每一個新實現的元件時,我們將在 Google 的程式碼庫中投入使用,這種逐步推出的方式使我們對新 API 的可用性、效能以及正確性都充滿了信心。我相信已經準備好可以在生產環境使用了。

我們很激動地看到這個版本的釋出,並且希望它能在未來十年甚至更長的時間內為 Go 生態系統持續服務。

相關文章

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章