Protocol BuffersとGo言語を組み合わせて使う場合の注意点

gRPCやConnectを用いてコンピュータ間通信を行う場合、Protocol Buffersによる通信フォーマットの定義が必要になる。

私自身、gRPCとConnectを少しかじった程度の知識しかないが、Bufを用いてConnect用のGo言語の関数定義を出力したときに

「ちょっとこれは使い方を気をつけないとな」

と感じたことがあるので以下メモとして残しておく。

今後、メモの必要を感じたら追記もあるかも。


列挙体のメンバーにはプレフィクスが必須

ご存知Go言語には「列挙体」(あるいは列挙型)という概念が無い。

その代わり、次のようにして似たような機能性が実現できる。

// Go言語における列挙体っぽいものの定義例

type MyEnum = int

const (
    Member0 MyEnum = iota
    Member1
    Member2
)

一方、Protocol Buffersにはしっかりと列挙体の概念がある。(Protocol Buffers Documentationより引用)

// Protocol Buffersにおける列挙体の定義例

enum Enum {
  A = 0;
  B = 1;
}

message Msg {
  optional Enum enum = 1;
}

ここで注意しなければならないのが、列挙体メンバーのスコープだ。

Go言語で上記のようにして定義した列挙体(っぽいもの)のメンバーのスコープは、列挙体の中で閉じないため、他の列挙体で同じ名前のメンバーを定義しようとすると名前が競合してコンパイルエラーとなってしまう。

具体例を挙げる。

Go言語で次のように2つの列挙体を定義した場合を考える。

// 以下の例では「Normal」という名前が同じスコープ内で2回定義されているのでコンパイルエラーとなる。

// 歯ブラシの硬さ
type BrushHardness = int

const (
    Normal BrushHardness = iota
    Soft
    Hard
)


// 電球の明るさ
type Brightness = int

const (
    Normal Brightness = iota    // BrushHardnessにもNormalが定義されている。コンパイルエラー!
    Dim
    Bright
)

この例の場合、

BrushHardness列挙体のNormalと、

Brightness列挙体のNormalは

同じスコープ上で定義されるため、名前の競合が起こりコンパイルエラーとなる。


そしてProtocol Buffersで列挙体を定義し、Buf等でGo言語にトランスパイルすると、

列挙体は上記のやり方と同様に列挙体っぽいものとして定義されるため、

列挙体のメンバーが同じスコープ上で定義されてしまう。

1箇所でも列挙体のメンバーの名前が被ったらProtocol Buffersのトランスパイルに失敗することとなる。

この問題に対する現実的な回避策は、見出しにもあるが次の通りだ。

列挙体のメンバーにプレフィクスを付ける

先ほどの「Normal」という名前が競合する例では次のようにして対処する。

// Go言語

// 歯ブラシの硬さ
type BrushHardness = int

// 各メンバーの名前に "BrushHardness_" というプレフィクスを付ける。
const (
    BrushHardness_Normal BrushHardness = iota
    BrushHardness_Soft
    BrushHardness_Hard
)


// 電球の明るさ
type Brightness = int

// 各メンバーの名前に "Brightness_" というプレフィクスを付ける。
const (
    Brightness_Normal Brightness = iota    // 名前の競合は起こらない。解決!
    Brightness_Dim
    Brightness_Bright
)

Protocol Buffersではこのような列挙体っぽいものを生成することを見越して、次のようにプレフィクスを付けた状態で列挙体を定義することになる。

// Protocol Buffers

// 歯ブラシの硬さ
enum BrushHardness {
    BrushHardness_Normal = 0;
    BrushHardness_Soft = 1;
    BrushHardness_Hard = 2;
}

// 電球の明るさ
enum Brightness {
    Brightness_Normal = 0;
    Brightness_Dim = 1;
    Brightness_Bright = 2;
}

こんなの冗長だ?名前が長くなりすぎて読みにくい?

気持ちはよく解かる。

しかし、今のところ現実的な解はこれしか見いだせていない。

次のようにProtocol Buffersのメッセージ内で定義した列挙体のスコープはメッセージ内で閉じるため、長い目で見てもこの性質が有用なら利用するのも手だ。

// Protocol Buffers

message MessageA {
    enum MyEnumA {
        Normal = 0;
    }
}

message MessageB {
    enum MyEnumB {
        Normal = 0;    // スコープがメッセージ内で閉じるため、名前の競合は起こらない。
    }
}

しかし、この方法を採用してしまうと現実的には後々の保守やアップデートの足を引っ張りかねないと感じている。

後からMessageA内に新たに列挙体定義が必要になって、その列挙体のメンバーに “Normal” が必要になったら?

また名前の競合が発生し、問題は振り出しに戻る。

それどころか、「やはりメンバーの名前にプレフィクスを付けるやり方に変えよう」となったら大きな手戻りにもなる。

やはり名前が冗長になっても、可読性を犠牲をしても、列挙体のメンバーにプレフィクスを付ける方法以外に現実的な解決策は無さそうだと思う。

コメントする