Protocol Buffersのoneofを使って実装しようとしたときに微妙に悩んだので、使用方法を備忘録として残しておきます。
Protocol Buffers、Go、gRpc、どれも触り始めて数ヶ月なので、変な箇所がありましたら指摘していただけると大変助かります。
oneofとは
あるメッセージで複数のフィールドを定義して、「このフィールドのうち、最大でどれか1個だけがセットされてるよ!」と宣言できる機能です。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
上記のようにメッセージを定義すると、「SampleMessage
にはname
フィールドかsub_message
フィールドのどちらかが入っている。どちらのフィールドも入ってない場合もある」ということになります。
ただし、name
とsub_message
の両方が入っていることはありません。
なお、repeated
なフィールドはoneof内では使えません。
oneof内で宣言されているフィールドは共有メモリを使うので、メモリの節約ができるそうです。
また、メッセージの一部を共通化したいときとか、
1つのAPIで異なるフィールドを扱いたいときに便利です。
あと、proto3でoptional
が使えなくなったので、optionalの代わりに使うという方法も提示されてます。
protoの作成
以下、oneofを使ったprotoファイルの例です。
// testOneOf.proto
// ...(略)
message OneOf{
oneof name{
int32 id = 1;
string first_name = 2;
string nickname = 3;
}
}
message Name{
string name = 1;
}
service TestOneOf {
rpc Get(google.protobuf.Empty) returns (OneOf) {
option (google.api.http) = {
get: "/oneof"
};
}
rpc Post(OneOf) returns (Name) {
option (google.api.http) = {
post: "/oneof"
body: "*"
};
}
}
OneOf
メッセージの中でoneofを使っています。
OneOf
メッセージの中身は、以下のうちのどれか一つになります。
- int型の
id
フィールド - string型の
first_name
フィールド - string型の
nickname
フィールド - フィールドなし
今回はフィールドなしの場合は置いとこうと思います。
Get
を実行すると空のメッセージかid
かfirst_name
かnickname
が入ってるメッセージを返すよ、
Post
を実行するときは上記のうちのどれかを送信してね、という感じです。
ちなみに、Post
実行時はリクエストで渡された値をstring型にして出力するように実装しようと思います。
作成したprotoファイルをprotoc
でコンパイルし、実装していきます。
oneofを使ってレスポンスを返す
oneofを使ってレスポンスを返すときは以下のように実装します。
// testOneOf.go
// ...(略)
// Get oneofを使ってレスポンスを返す
func (t *TestOneOf) Get(ctx context.Context, in *empty.Empty) (*proto.OneOf, error) {
return &proto.OneOf{
Name: &proto.OneOf_FirstName{
FirstName: "Andy",
},
}, nil
}
上記のように実装してAPIを実行すると、以下のようなレスポンスが返ってきます。
(私の環境ではgrpc-gatewayを使用しているので、curlコマンドからAPIを実行しました。)
$ curl -X GET "http://localhost/oneof"
{"first_name":"Andy"}
first_name
が返ってきましたね!
上記Get
関数のreturn
部分を以下のように変えると、int型のId
を返すことができます。
return &proto.OneOf{
Name: &proto.OneOf_Id{
Id: 123,
},
}, nil
変更後のAPI実行結果は以下の通り。
$ curl -X GET "http://localhost/oneof"
{"id":123}
フィールドなしのメッセージを返したい場合は以下のように変更します。
return &proto.OneOf{}, nil
変更後のAPI実行結果は以下の通り。
$ curl -X GET "http://localhost/oneof"
{}
oneofフィールドの構造体
ところで「return
文で使ってるOneOf_FirstName
ってどこから来たんだ、そんな構造体protoファイルで宣言してないぞ」となりませんでしたか?
私はなりました。
そこで、コンパイルで自動生成された*.pg.go
ファイルの中身をちょっと覗いてみました。
// testOneOf.pb.go
// ...(略)
type OneOf struct {
// Types that are valid to be assigned to Name:
// *OneOf_Id
// *OneOf_FirstName
// *OneOf_Nickname
Name isOneOf_Name `protobuf_oneof:"name"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
// ...(略)
type isOneOf_Name interface {
isOneOf_Name()
}
type OneOf_Id struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3,oneof"`
}
type OneOf_FirstName struct {
FirstName string `protobuf:"bytes,2,opt,name=first_name,json=firstName,proto3,oneof"`
}
type OneOf_Nickname struct {
Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3,oneof"`
}
// ...(略)
func (*OneOf_Id) isOneOf_Name() {}
func (*OneOf_FirstName) isOneOf_Name() {}
func (*OneOf_Nickname) isOneOf_Name() {}
// ...(略)
構造体OneOf_FirstName
が自動生成されてました。
OneOf.Name
にはisOneOf_Name
というインタフェース型を指定するようになっており、自動生成された構造体を動的に扱えるようになっていますね。
こういう使い方もできるんだなあ、Goのインタフェース…。
これに気付かず、いざoneofを使おう!というときに「どうやってfirst_name
返したらいいんだこれ???」とちょっと悩んでしまいました。。。
公式ドキュメントには「oneof内のフィールドごとに構造体が生成されるよ!」とちゃんと書いてあります。
For a oneof field, the protobuf compiler generates a single field with an interface type isMessageName_MyField. It also generates a struct for each of the singular fields within the oneof. These all implement this isMessageName_MyField interface.
やっぱりドキュメントは読まないといけない。
oneofを使ってリクエストを受け付ける
リクエストを受け付けるときは以下のように実装します。
// testOneOf.go
// ...(略)
// Post oneofを使ってリクエストを受け付ける
func (t *TestOneOf) Post(ctx context.Context, in *proto.OneOf) (*proto.Name, error) {
name := in.GetName()
switch x := name.(type) {
case *proto.OneOf_Id:
return &proto.Name{
Name: strconv.Itoa(int(x.Id)),
}, nil
case *proto.OneOf_FirstName:
return &proto.Name{
Name: x.FirstName,
}, nil
case *proto.OneOf_Nickname:
return &proto.Name{
Name: x.Nickname,
}, nil
default:
return nil, nil
}
}
引数in
からGetName()
を使ってoneof name
の部分にあたる値を取り出し、
型スイッチでどの構造体が使われているか判定して、しかるべき処理をするという感じです。
上記のように実装してAPIを実行してみます。
$ curl -X POST "http://localhost/oneof" -d '{"first_name": "Bob"}'
{"name":"Bob"}
first_name
として送信したBobが返ってきました!
nickname
でも同様です。
$ curl -X POST "http://localhost/oneof" -d '{"nickname": "Bobby"}'
{"name":"Bobby"}
int型のid
でも返ってきました。
$ curl -X POST "http://localhost/oneof" -d '{"id": 123}'
{"name":"123"}
型スイッチでnil
だったときの処理をちゃんと実装していないので、空のメッセージを送信するとエラーになります。
$ curl -X POST "http://local.yj-webapp.xyz/oneof" -d '{}'
{"error":"grpc: error while marshaling: proto: Marshal called with nil","code":13,"error_details":null}
oneof内フィールドの取得
Post
の実装でGetName()
というメソッドを使ってます。
これもコンパイル時に自動生成されたものなので、*.pb.go
を覗いて中で何をやってるメソッドなのかを確認してみました。
// testOneOf.pb.go
// ...(略)
func (m *OneOf) GetName() isOneOf_Name {
if m != nil {
return m.Name
}
return nil
}
// ...(略)
OneOf
メッセージ内のName
を返してます。まあそうだろうなという感じです。
このくらい自力でも実装できるでしょうが、こうやって自動生成で用意してくれるのは手間が省けてありがたいなと思いました。
以下のように、OneOf
メッセージから直でId
とかを返してくれるメソッドも用意されてるみたいです。
// testOneOf.pb.go
// ...(略)
func (m *OneOf) GetId() int32 {
if x, ok := m.GetName().(*OneOf_Id); ok {
return x.Id
}
return 0
}
func (m *OneOf) GetFirstName() string {
if x, ok := m.GetName().(*OneOf_FirstName); ok {
return x.FirstName
}
return ""
}
func (m *OneOf) GetNickname() string {
if x, ok := m.GetName().(*OneOf_Nickname); ok {
return x.Nickname
}
return ""
}
// ...(略)
直で取得したいな!という場面があるかどうかわからないですが便利。たぶん。