17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Protocol Buffersのoneofを使う【Go + gRPC】

Last updated at Posted at 2019-11-07

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フィールドのどちらかが入っている。どちらのフィールドも入ってない場合もある」ということになります。
ただし、namesub_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を実行すると空のメッセージかidfirst_namenicknameが入ってるメッセージを返すよ、
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 ""
}

// ...(略)

直で取得したいな!という場面があるかどうかわからないですが便利。たぶん。

参考文献

17
10
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
17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?