LoginSignup
0
1

Protocol BuffersのOptionをGo側で読み取る方法を考える

Last updated at Posted at 2024-01-24

はじめに

Protocol Buffersでは、Enumを宣言してGoのコードを生成すると、基本的には stringint32 の対応関係が生成される。しかし、この 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

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 型の Descgoogle.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;
}

ref: https://github.com/protocolbuffers/protobuf/blob/v21.3/src/google/protobuf/descriptor.proto#L246-L273

そして、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を設計できるようになりたいものだなと思った。

  1. 例えば、認可グラントをEnumで表現したい場合、urn:ietf:params:oauth:grant-type:jwt-bearer のような値を設定することはできない。

  2. protoc-gen-goはv1.32.0、protocはv4.23.4を利用した。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1