03.04 Go 官宣:新版 Protobuf API

原文作者:Joe Tsai, Damien Neil 和 Herbie Ong

原文鏈接:https://blog.golang.org/a-new-go-api-for-protocol-buffers

發佈時間:2020-03-02

翻譯:polaris,Go 語言中文網


Go 官宣:新版 Protobuf API


簡介

我們很高興地宣佈,用於 Google 的語言無關數據交換格式 protocol buffers[1] 的新版 Go API 發佈了,這是一次重大的版本升級。

新 API 的動機

Go 的第一個 protocol buffer 綁定由 Rob Pike 於 2010 年 3 月宣佈[2]。兩年後 Go1 才發佈。

自從首次發佈以來的十年中,該包與 Go 一起發展壯大。它的用戶需求也有所增長。

許多人想編寫使用反射來檢查 protocol buffer 的程序。反射包[3]提供了 Go 類型和值的視圖,但是忽略了 protocol buffer 類型系統中的信息。例如,我們可能想編寫一個遍歷日誌條目並清除任何註解(annotation)為包含敏感數據的字段的函數。註解不是 Go 類型系統的一部分。

另一個常見的需求是使用 protocol buffer 編譯器生成的數據結構之外的其他數據結構,例如能夠代表其消息類型在編譯時未知的動態消息類型。

我們還觀察到,常見的問題根源是 proto.Message[4] 接口,它標識生成的消息類型的值,對描述這些類型的行為幾乎沒有作用。當用戶創建實現該接口的類型(通常通過在另一個結構中嵌入消息而無意間實現)並將這些類型的值傳遞給期望生成消息值的函數時,程序崩潰或行為異常。

以上這三個問題都有一個共同的原因,並且有一個共同的解決方案:Message 接口應完全指定消息的行為,並且對 Message 值進行操作的函數應自由接受可以正確實現該接口的任何類型。

由於在保持包 API 兼容的同時無法更改 Message 類型的現有定義,因此我們決定是時候開始研究 protobuf 模塊的新的,不兼容的主要版本了。

今天,我們很高興發佈該新模塊。我們希望您能喜歡。

反射

反射是新實現的旗艦特性。類似於 reflect 包如何提供 Go 類型和值的視圖, google.golang.org/protobuf/reflect/protoreflect[5] 包提供了根據 protocol buffer 類型系統的值視圖。

對 protoreflect 包的完整描述對於這篇文章來說可能會花很長時間,但是讓我們看一下如何編寫我們前面提到的 log-scrubbing 函數。

首先,我們將編寫一個 .proto 文件,該文件定義 google.protobuf.FieldOptions[6] 類型的擴展名,以便我們可以將字段註解為包含或不包含敏感信息。

<code>syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
bool non_sensitive = 50000;
}/<code>

我們可以使用此選項將某些字段標記為不敏感。

<code>message MyMessage {
string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}/<code>

接下來,我們將編寫一個 Go 函數,該函數接受任意消息值並刪除所有敏感字段。

<code>// Redact clears every sensitive field in pb.
func Redact(pb proto.Message) {
// ...
}/<code>

此函數接受 proto.Message[7],這是由所有生成的消息類型實現的接口類型。此類型是 protoreflect 包中定義的別名:

<code>type ProtoMessage interface{
ProtoReflect() Message
}/<code>

為了避免填充所生成消息的名稱空間,該接口僅包含一個返回 protoreflect.Message[8]的方法,該方法提供對消息內容的訪問。

(為什麼要使用別名?因為 protoreflect.Message 具有返回原始 proto.Message 的相應方法,所以我們需要避免兩個包之間的循環導入。)

protoreflect.Message.Range[9] 方法為消息中的每個填充字段調用一個函數。

<code>m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
// ...
return true
})/<code>

使用描述字段的 protocol buffer 類型的 protoreflect.FieldDescriptor[10] 和包含字段值的 protoreflect.Value[11] 來調用 range 函數。

protoreflect.FieldDescriptor.Options[12] 方法以 google.protobuf.FieldOptions 消息的形式返回字段選項。

<code>opts := fd.Options().(*descriptorpb.FieldOptions)/<code>

(為什麼要使用類型斷言?由於生成的描述符 pb 包依賴於 protoreflect,因此 protoreflect 包不能在不引起循環導入的情況下返回具體的選項類型。)

然後,我們可以檢查選項以查看擴展布爾值:

<code>if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true // don't redact non-sensitive fields
}/<code>

請注意,我們在這裡查看的是字段描述符,而不是字段值。我們感興趣的信息在於 protocol buffer 類型系統,而不是 Go 語言。

這也是我們簡化了 proto 包 API 的一個示例。原來的 proto.GetExtension[13] 返回一個值和一個錯誤。新的proto.GetExtension[14] 僅返回一個值,如果不存在該字段,則返回默認值。在 Unmarshal 時間時報告擴展解碼錯誤。

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

<code>m.Clear(fd)/<code>

綜上所述,我們完整的修改功能是:

<code>// Redact clears every sensitive field in 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
})
}/<code>

更完整的實現可以遞歸地深入消息值字段。我們希望這個簡單的例子能使您瞭解 protocol buffer 反射及其用法。

版本號

我們將 Go protocol buffers 的原始版本稱為 APIv1,將新版本稱為 APIv2。由於 APIv2 與 APIv1 不向後兼容,因此我們需要為每個 APIv 使用不同的模塊路徑。

(這些 API 版本與 protocol buffers 語言的版本不同:proto1,proto2 和 proto3。APIv1 和 APIv2 是 Go 中的具體實現,均支持 proto2 和 proto3 語言版本。)

github.com/golang/protobuf[15] 是模塊 APIv1 版本。

google.golang.org/protobuf[16] 模塊是 APIv2。我們利用了需要更改導入路徑以切換到與特定託管服務提供商無關的路徑的優勢。(我們考慮了 google.golang.org/protobuf/v2,以明確說明這是該 API 的第二個主要版本,但從長遠來看,選擇較短的路徑是更好的選擇。)

我們知道,並非所有用戶都將以相同的速度遷移到包的新主版本。有些會快速切換;其他可能會無限期保留在舊版本中。即使在一個程序中,某些部分可能使用一個 API,而其他部分則使用另一個。因此,我們必須繼續支持使用 APIv1 的程序。

  • github.com/golang/[email protected] 是 APIv1 最新 pre-APIv2 版本。
  • github.com/golang/[email protected] 是根據 APIv2 實現的 APIv1 版本。它的 API 相同,但是基礎實現由新的實現支持。此版本包含在 APIv1 和 APIv2 proto.Message 接口之間轉換的函數,以簡化兩者之間的過渡。
  • google.golang.org/[email protected] 是 APIv2。該模塊依賴 github.com/golang/[email protected],因此任何使用 APIv2 的程序都會自動選擇與其集成的 APIv1 版本。

(為什麼要從 v1.20.0 版本開始?為了清楚起見。我們認為 APIv1 不會達到 v1.20.0,因此僅版本號就應該足以區分 APIv1 和 APIv2。)

我們打算無限期地保持對 APIv1 的支持。

該組織確保任何給定程序都將僅使用單個 protocol buffer 實現,而不管其使用哪個 API 版本。它允許程序逐漸或根本不採用新的 API,同時仍獲得新實現的優點。最小版本選擇的原則意味著程序可以保留在舊的實現上,直到維護者選擇更新到新的(直接或通過更新依賴項)。

值得關注的其他特性

google.golang.org/protobuf/encoding/protojson[17] 包使用規範的 JSON 映射將 protocol buffer 消息與 JSON 相互轉換,並解決了舊 jsonpb 包難以解決的許多問題,而這些問題不會對現有用戶造成問題。

google.golang.org/protobuf/types/dynamicpb[18] 包為 protocol buffer 類型在運行時派生的消息提供了 proto.Message 的實現。

google.golang.org/protobuf/testing/protocmp[19] 包提供了將 protocol buffer 消息與 github.com/google/cmp[20] 包進行比較的功能。

google.golang.org/protobuf/compiler/protogen[21] 包提供了對編寫協議編譯器插件的支持。

總結

google.golang.org/protobuf 模塊是 Go 對 protocol buffers 支持的主要改進,它為反射,自定義消息實現和清理的 API surface 提供了優先支持。我們打算無限期地維護以前的 API 作為新 API 的包裝,從而使用戶可以按照自己的步調逐步採用新 API。

我們在此更新的目標是在解決舊 API 的缺點的同時,提升舊 API 的優勢。完成新實現的每個組件後,我們將其在 Google 的代碼庫中投入使用。這種逐步推出的方式使我們對新 API 的可用性以及新實現的性能和正確性都充滿信心。我們相信已經準備好用於生產環境了。

我們很興奮地發佈了該版本,並希望它將在未來十年甚至更長時間內為 Go 生態系統服務!

[1]

protocol buffers: https://developers.google.com/protocol-buffers

[2]

Rob Pike 於 2010 年 3 月宣佈: https://blog.golang.org/third-party-libraries-goprotobuf-and

[3]

反射包: http://docs.studygolang.com/pkg/reflect

[4]

proto.Message: https://pkg.go.dev/github.com/golang/protobuf/proto?tab=doc#Message

[5]

google.golang.org/protobuf/reflect/protoreflect: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc

[6]

google.protobuf.FieldOptions: https://github.com/protocolbuffers/protobuf/blob/b96241b1b716781f5bc4dc25e1ebb0003dfaba6a/src/google/protobuf/descriptor.proto#L509

[7]

proto.Message: https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#Message

[8]

protoreflect.Message: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message

[9]

protoreflect.Message.Range: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message.Range

[10]

protoreflect.FieldDescriptor: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#FieldDescriptor

[11]

protoreflect.Value: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Value

[12]

protoreflect.FieldDescriptor.Options: https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Descriptor.Options

[13]

proto.GetExtension: https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#GetExtension

[14]

proto.GetExtension: https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#GetExtension

[15]

github.com/golang/protobuf: https://pkg.go.dev/github.com/golang/protobuf?tab=overview

[16]

google.golang.org/protobuf: https://pkg.go.dev/google.golang.org/protobuf?tab=overview

[17]

google.golang.org/protobuf/encoding/protojson: https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson

[18]

google.golang.org/protobuf/types/dynamicpb: https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb

[19]

google.golang.org/protobuf/testing/protocmp: https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp

[20]

github.com/google/cmp: https://pkg.go.dev/github.com/google/go-cmp/cmp

[21]

google.golang.org/protobuf/compiler/protogen: https://pkg.go.dev/google.golang.org/protobuf/compiler/protogen?tab=doc


分享到:


相關文章: