はじめに
Protocol Buffersでは、Enumを宣言してGoのコードを生成すると、基本的には string
↔ int32
の対応関係が生成される。しかし、この string
に使える文字種は、Specificationによると、 [A-Za-z_]
の範囲に限られる。そのため、これ以外の文字種を使いたい場合は、他の方法で表現する必要がある1。
変換ロジックを外部に置くことも出来るが、その場合は各言語で実装することになるため、対応関係に差異が生じる危険性がある。そこで、オプショナルな値をProtocol Buffers側で宣言し、各実装でオプションの値を取得する方法を本記事では紹介する。
実際の例を考える
具体例が理解の助けになると思うので、色を表すEnum定義と、定義に該当するカラーコードを持たせる例について考える。本記事で利用するコードはリポジトリで公開しているので、必要に応じて参照して欲しい。
さて、Specificationによると、Optionを設定するためにはEnumValueにEnumValueOptionsを持たせれば良いので、次のようにProtocol Buffersを定義する。
// EnumValueOptionsを拡張
extend google.protobuf.EnumValueOptions {
optional ColorOptions color_options = 50001;
}
// Enum用のOption値
message ColorOptions {
string color_code = 1;
}
// 色を表すEnumと、そのオプション定義
enum Color {
WHITE = 0 [ (color_options) = {color_code: "#FFFFFF" } ];
BLACK = 1 [ (color_options) = {color_code: "#000000" } ];
}
このコードを用意して生成されるコードは次の通りだ2。
color.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
// protoc v4.23.4
// source: proto/color.proto
package generated
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Color int32
const (
Color_WHITE Color = 0
Color_BLACK Color = 1
)
// Enum value maps for Color.
var (
Color_name = map[int32]string{
0: "WHITE",
1: "BLACK",
}
Color_value = map[string]int32{
"WHITE": 0,
"BLACK": 1,
}
)
func (x Color) Enum() *Color {
p := new(Color)
*p = x
return p
}
func (x Color) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Color) Descriptor() protoreflect.EnumDescriptor {
return file_proto_color_proto_enumTypes[0].Descriptor()
}
func (Color) Type() protoreflect.EnumType {
return &file_proto_color_proto_enumTypes[0]
}
func (x Color) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Color.Descriptor instead.
func (Color) EnumDescriptor() ([]byte, []int) {
return file_proto_color_proto_rawDescGZIP(), []int{0}
}
type ColorOptions struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ColorCode string `protobuf:"bytes,1,opt,name=color_code,json=colorCode,proto3" json:"color_code,omitempty"`
}
func (x *ColorOptions) Reset() {
*x = ColorOptions{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_color_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ColorOptions) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ColorOptions) ProtoMessage() {}
func (x *ColorOptions) ProtoReflect() protoreflect.Message {
mi := &file_proto_color_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ColorOptions.ProtoReflect.Descriptor instead.
func (*ColorOptions) Descriptor() ([]byte, []int) {
return file_proto_color_proto_rawDescGZIP(), []int{0}
}
func (x *ColorOptions) GetColorCode() string {
if x != nil {
return x.ColorCode
}
return ""
}
var file_proto_color_proto_extTypes = []protoimpl.ExtensionInfo{
{
ExtendedType: (*descriptorpb.EnumValueOptions)(nil),
ExtensionType: (*ColorOptions)(nil),
Field: 50001,
Name: "sample.color_options",
Tag: "bytes,50001,opt,name=color_options",
Filename: "proto/color.proto",
},
}
// Extension fields to descriptorpb.EnumValueOptions.
var (
// optional sample.ColorOptions color_options = 50001;
E_ColorOptions = &file_proto_color_proto_extTypes[0]
)
var File_proto_color_proto protoreflect.FileDescriptor
var file_proto_color_proto_rawDesc = []byte{
0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x06, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x1a, 0x20, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73,
0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 0x0a,
0x0c, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a,
0x0a, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x2a, 0x3b, 0x0a, 0x05,
0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x05, 0x57, 0x48, 0x49, 0x54, 0x45, 0x10, 0x00,
0x1a, 0x0d, 0x8a, 0xb5, 0x18, 0x09, 0x0a, 0x07, 0x23, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x12,
0x18, 0x0a, 0x05, 0x42, 0x4c, 0x41, 0x43, 0x4b, 0x10, 0x01, 0x1a, 0x0d, 0x8a, 0xb5, 0x18, 0x09,
0x0a, 0x07, 0x23, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3a, 0x61, 0x0a, 0x0d, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75,
0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd1, 0x86,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x43,
0x6f, 0x6c, 0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0c, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x5a, 0x0c,
0x2e, 0x2e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_color_proto_rawDescOnce sync.Once
file_proto_color_proto_rawDescData = file_proto_color_proto_rawDesc
)
func file_proto_color_proto_rawDescGZIP() []byte {
file_proto_color_proto_rawDescOnce.Do(func() {
file_proto_color_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_color_proto_rawDescData)
})
return file_proto_color_proto_rawDescData
}
var file_proto_color_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proto_color_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proto_color_proto_goTypes = []interface{}{
(Color)(0), // 0: sample.Color
(*ColorOptions)(nil), // 1: sample.ColorOptions
(*descriptorpb.EnumValueOptions)(nil), // 2: google.protobuf.EnumValueOptions
}
var file_proto_color_proto_depIdxs = []int32{
2, // 0: sample.color_options:extendee -> google.protobuf.EnumValueOptions
1, // 1: sample.color_options:type_name -> sample.ColorOptions
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
1, // [1:2] is the sub-list for extension type_name
0, // [0:1] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_proto_color_proto_init() }
func file_proto_color_proto_init() {
if File_proto_color_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_color_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ColorOptions); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_color_proto_rawDesc,
NumEnums: 1,
NumMessages: 1,
NumExtensions: 1,
NumServices: 0,
},
GoTypes: file_proto_color_proto_goTypes,
DependencyIndexes: file_proto_color_proto_depIdxs,
EnumInfos: file_proto_color_proto_enumTypes,
MessageInfos: file_proto_color_proto_msgTypes,
ExtensionInfos: file_proto_color_proto_extTypes,
}.Build()
File_proto_color_proto = out.File
file_proto_color_proto_rawDesc = nil
file_proto_color_proto_goTypes = nil
file_proto_color_proto_depIdxs = nil
}
結論を先に書くと、次のように取得できる。
package main
import (
"log"
"gihtub.com/task4233/protoc/generated"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
)
func GetOption(x generated.Color) *generated.ColorOptions {
return proto.GetExtension(
x.Descriptor().Values().Get(int(x.Number())).Options().(*descriptorpb.EnumValueOptions),
generated.E_ColorOptions,
).(*generated.ColorOptions)
}
func main() {
log.Printf("optional_value: %s", GetOption(generated.Color_BLACK).GetColorCode())
}
とはいえ、内部の仕組みについて知りたい方もいると思うので、順を追って書く。
Extensionへのアクセス
protoのGoDocを見ると、Extensionについて次のように言及されている。
Extension accessors ¶
- HasExtension, GetExtension, SetExtension, and ClearExtension
access extension field values in a protocol buffer message.Extension fields are only supported in proto2.
proto2でしかサポートされていないらしいが、ひとまずGetExtension関数で取得できそうだ。GetExtension関数の定義を見ると、 proto.Message
すなわち protoreflect.ProtoMessage
インタフェースを満たしている引数と、 protoreflect.ExtensionType
インタフェースを満たしている引数を必要とする。したがって、これらの値をどのように取得するか考えれば良い。
まず、 protore.Message
は欲しいExtensionのMessage情報を取得すれば良いので、EnumValueのOptionsを利用する。この取得方法は後述する。
次に、 protoreflect.ExtensionType
は欲しいExtensionのTypeを取得すれば良いので、生成された .pb.go
に含まれている定義を流用する。上記の color.pb.go
では次の通り定義されている。
var file_proto_color_proto_extTypes = []protoimpl.ExtensionInfo{
{
ExtendedType: (*descriptorpb.EnumValueOptions)(nil),
ExtensionType: (*ColorOptions)(nil),
Field: 50001,
Name: "sample.color_options",
Tag: "bytes,50001,opt,name=color_options",
Filename: "proto/color.proto",
},
}
// Extension fields to descriptorpb.EnumValueOptions.
var (
// optional sample.ColorOptions color_options = 50001;
E_ColorOptions = &file_proto_color_proto_extTypes[0]
)
したがって、1つ目の proto.Mesasge
をどのように取得するかを考える。そのために、Protocol BuffersからGoコードがどのように生成されるかに触れる必要があるので、次はそれについて書く。
Protocol BuffersからGoコードの生成まで
今回はEnumの話なので、Protocol Buffersから生成されるGoのコードのうちEnumのみに絞って書く。他の型においても多少は異なるかもしれないが、似たような実装になっているので各自実装を追って欲しい。
ちなみに、protoreflectパッケージを見ると、それぞれの型の関係が図式化してあるので、理解の助けになるかもしれない。
// ┌───────────────────────────────────┐
// V │
// ┌────────────── New(n) ─────────────┐ │
// │ │ │
// │ ┌──── Descriptor() ──┐ │ ┌── Number() ──┐ │
// │ │ V V │ V │
// ╔════════════╗ ╔════════════════╗ ╔════════╗ ╔════════════╗
// ║ EnumType ║ ║ EnumDescriptor ║ ║ Enum ║ ║ EnumNumber ║
// ╚════════════╝ ╚════════════════╝ ╚════════╝ ╚════════════╝
// Λ Λ │ │
// │ └─── Descriptor() ──┘ │
// │ │
// └────────────────── Type() ───────┘
ref: https://pkg.go.dev/google.golang.org/protobuf@v1.32.0/reflect/protoreflect#hdr-Relationships
それはさておき、Protocol BuffersのEnum型から生成されるGoのコードは、 impl
パッケージの EnumInfo
構造体によって表現される。
type EnumInfo struct {
GoReflectType reflect.Type // int32 kind
Desc protoreflect.EnumDescriptor
}
ref: https://pkg.go.dev/google.golang.org/protobuf@v1.32.0/internal/impl#EnumInfo
これを見るとEnumInfoは型情報とDescriptorを持っていることが分かる。
reflect.Type
型の GoReflectType
は型情報を持つ。このフィールドには、ここでGoコードのBuild時に生成されたGoの型が入るようになっている。今回は Color
型が int32
なので int32
が入る。DescriptorはGoで生成されている次の rawDesc
をパースして生成される。
rawDescのバイト列
var file_proto_color_proto_rawDesc = []byte{
0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x06, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x1a, 0x20, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73,
0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 0x0a,
0x0c, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a,
0x0a, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x2a, 0x3b, 0x0a, 0x05,
0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x05, 0x57, 0x48, 0x49, 0x54, 0x45, 0x10, 0x00,
0x1a, 0x0d, 0x8a, 0xb5, 0x18, 0x09, 0x0a, 0x07, 0x23, 0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x12,
0x18, 0x0a, 0x05, 0x42, 0x4c, 0x41, 0x43, 0x4b, 0x10, 0x01, 0x1a, 0x0d, 0x8a, 0xb5, 0x18, 0x09,
0x0a, 0x07, 0x23, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3a, 0x61, 0x0a, 0x0d, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75,
0x6d, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd1, 0x86,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x43,
0x6f, 0x6c, 0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0c, 0x63, 0x6f, 0x6c,
0x6f, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x5a, 0x0c,
0x2e, 0x2e, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
HexCodeのままASCIIコードが読める変人天才ならば、これらにProtocol Buffersで設定したOption値が設定されていることが分かる。実際に、protocでデコードすると次のようにパースできる。
パース結果
$ echo 0a1170726f746f2f636f6c6f722e70726f746f120673616d706c651a20676f6f676c652f70726f746f6275662f64657363726970746f722e70726f746f222d0a0c436f6c6f724f7074696f6e73121d0a0a636f6c6f725f636f64651801200128095209636f6c6f72436f64652a3b0a05436f6c6f7212180a05574849544510001a0d8ab518090a072346464646464612180a05424c41434b10011a0d8ab518090a07233030303030303a610a0d636f6c6f725f6f7074696f6e7312212e676f6f676c652e70726f746f6275662e456e756d56616c75654f7074696f6e7318d186032001280b32142e73616d706c652e436f6c6f724f7074696f6e73520c636f6c6f724f7074696f6e73880101420e5a0c2e2e2f67656e657261746564620670726f746f33 | xxd -r -p | protoc --decode_raw
1: "proto/color.proto"
2: "sample"
3: "google/protobuf/descriptor.proto"
4 {
1: "ColorOptions"
2 {
1: "color_code"
3: 1
4: 1
5: 9
10: "colorCode"
}
}
5 {
1: "Color"
2 {
1: "WHITE"
2: 0
3 {
50001 {
1: "#FFFFFF"
}
}
}
2 {
1: "BLACK"
2: 1
3 {
50001 {
1: "#000000"
}
}
}
}
7 {
1: "color_options"
2: ".google.protobuf.EnumValueOptions"
3: 50001
4: 1
5: 11
6: ".sample.ColorOptions"
10: "colorOptions"
17: 1
}
8 {
11: "../generated"
}
12: "proto3"
フォーマットに関してはドキュメントや実装を見れば理解できるはずなので、興味のある人は参照して欲しい。
また、 protoreflect.EnumDescriptor
型の Desc
は google.protobuf.EnumDescriptorProto
messsageに対応する型で、次の通り、値やオプションの情報などを持つことが分かる。
// Describes an enum type.
message EnumDescriptorProto {
optional string name = 1;
repeated EnumValueDescriptorProto value = 2;
optional EnumOptions options = 3;
// Range of reserved numeric values. Reserved values may not be used by
// entries in the same enum. Reserved ranges may not overlap.
//
// Note that this is distinct from DescriptorProto.ReservedRange in that it
// is inclusive such that it can appropriately represent the entire int32
// domain.
message EnumReservedRange {
optional int32 start = 1; // Inclusive.
optional int32 end = 2; // Inclusive.
}
// Range of reserved numeric values. Reserved numeric values may not be used
// by enum values in the same enum declaration. Reserved ranges may not
// overlap.
repeated EnumReservedRange reserved_range = 4;
// Reserved enum value names, which may not be reused. A given name may only
// be reserved once.
repeated string reserved_name = 5;
}
そして、Goの型定義は次の通りだ。
// EnumDescriptor describes an enum and
// corresponds with the google.protobuf.EnumDescriptorProto message.
//
// Nested declarations:
// [EnumValueDescriptor].
type EnumDescriptor interface {
Descriptor
// Values is a list of nested enum value declarations.
Values() EnumValueDescriptors
// ReservedNames is a list of reserved enum names.
ReservedNames() Names
// ReservedRanges is a list of reserved ranges of enum numbers.
ReservedRanges() EnumRanges
isEnumDescriptor
}
ここで、Values()、ReservedNames()、ReversedRanges()、Descriptor、isEnumDesciriptor interfaceはどこで実装されているのか、と思うかもしれない。実は、これらのinterfaceは、全てfiledescパッケージの中で宣言されている Enum
structおよびembedded fieldの Base
structで実装されている(ref)。そして、このstructに対して上記のrawDescのバイト列をパースして値がマップされるように実装されている。
Optional Valueの取り方
上記の定義からEnumのOptional ValueはValues()に紐づいた情報であることが分かる。したがって、Enum→Descriptor→Valuesから、各valueの値を取得すれば良さそうだ。ここで、ValuesからValueのフィルタ方法について考えたい。Valuesは次のinterfaceを持つ。
// EnumValueDescriptors is a list of enum value declarations.
type EnumValueDescriptors interface {
// Len reports the number of enum values.
Len() int
// Get returns the ith EnumValueDescriptor. It panics if out of bounds.
Get(i int) EnumValueDescriptor
// ByName returns the EnumValueDescriptor for the enum value named s.
// It returns nil if not found.
ByName(s Name) EnumValueDescriptor
// ByNumber returns the EnumValueDescriptor for the enum value numbered n.
// If multiple have the same number, the first one defined is returned
// It returns nil if not found.
ByNumber(n EnumNumber) EnumValueDescriptor
doNotImplement
}
これを見るに、管理されているEnum valuesの番号、もしくは名前で参照することが出来るようだ。そのため、対象のEnumの変数名を x
とした場合、次のように取得できるだろう。
x.Descriptor().Values().Get(int(x.Number())
x.Descriptor().Values().ByName(string(x))
-
x.Descriptor().Values().ByNumber(x.Number())
- aliasを利用する場合、適切ではないことに注意
この辺りはお好みだと思うが、ByNameを用いると string
への変換コストがかかるので、無難なのは1番目の取得方法になると思う。ここから、次のようにOptional Valueを取得すれば良さそうだ。
x.Descriptor().Values().Get(int(x.Number()).Options().(*descriptorpb.EnumValueOptions)
これで、無事に当初の目的であった proto.Message
が取得できた。
総括
これでExtensionの値を取得でき、次の通り無事に値を取得できる。
func GetOption(x generated.Color) *generated.ColorOptions {
defer func() {
if err := recover(); err != nil {
log.Printf("panic with %v", err)
}
}()
return proto.GetExtension(
x.Descriptor().Values().Get(int(x.Number())).Options().(*descriptorpb.EnumValueOptions),
generated.E_ColorOptions,
).(*generated.ColorOptions)
}
func main() {
log.Printf("optional_value: %s", GetOption(generated.Color_BLACK).GetColorCode())
}
おわりに
今回は、ふと疑問を持ったのでprotobuf関連のGoパッケージの実装に色々と目を通してみた。その過程で、Protocol Buffersに対応するGoの実装におけるinterfaceの使い方が上手いなと思った。
例えば、今回取り上げたEnumは保持しているDescriptorをinterfaceとして持っていたことを覚えているだろうか。同様に、Message型でも同じようにDescriptorをMessageDescriptorというinterfaceで持っていることが分かる。
// MessageInfo provides protobuf related functionality for a given Go type
// that represents a message. A given instance of MessageInfo is tied to
// exactly one Go type, which must be a pointer to a struct type.
//
// The exported fields must be populated before any methods are called
// and cannot be mutated after set.
type MessageInfo struct {
// GoReflectType is the underlying message Go type and must be populated.
GoReflectType reflect.Type // pointer to struct
// Desc is the underlying message descriptor type and must be populated.
Desc protoreflect.MessageDescriptor
// Exporter must be provided in a purego environment in order to provide
// access to unexported fields.
Exporter exporter
// OneofWrappers is list of pointers to oneof wrapper struct types.
OneofWrappers []interface{}
initMu sync.Mutex // protects all unexported fields
initDone uint32
reflectMessageInfo // for reflection implementation
coderMessageInfo // for fast-path method implementations
}
こういった綺麗なinterfaceを設計できるようになりたいものだなと思った。