Go Opaque API: 手動での移行

Opaque API への手動移行について説明します。

Opaque API は、Go プログラミング言語向けの Protocol Buffers 実装の最新バージョンです。以前のバージョンは Open Struct API と呼ばれていました。導入については、Go Protobuf: Releasing the Opaque API のブログ記事をご覧ください。

これは、Go Protobuf の使用を古い Open Struct API から新しい Opaque API に移行するためのユーザーガイドです。

生成されたコードガイドは、より詳細な情報を提供します。このガイドでは、古い API と新しい API を並べて比較します。

メッセージの構築

次のように定義された protobuf メッセージがあるとします。

message Foo {
  uint32 uint32 = 1;
  bytes bytes = 2;
  oneof union {
    string    string = 4;
    MyMessage message = 5;
  }
  enum Kind {  };
  Kind kind = 9;
}

リテラル値からこのメッセージを構築する方法の例を次に示します。

Open Struct API (旧)Opaque API (新)
m := &pb.Foo{
  Uint32: proto.Uint32(5),
  Bytes:  []byte("hello"),
}
m := pb.Foo_builder{
  Uint32: proto.Uint32(5),
  Bytes:  []byte("hello"),
}.Build()

ご覧のとおり、ビルダー構造体を使用すると、Open Struct API (旧) と Opaque API (新) の間でほぼ 1 対 1 の変換が可能です。

一般的に、可読性のためにビルダーを使用することを推奨します。ホットな内部ループで Protobuf メッセージを作成するようなまれなケースでのみ、ビルダーの代わりにセッターを使用する方が良い場合があります。詳細については、Opaque API FAQ: ビルダーとセッターのどちらを使用すべきですか?をご覧ください。

上記の例の例外は、oneofs を扱う場合です。Open Struct API (旧) は各 oneof ケースにラッパー構造体型を使用するのに対し、Opaque API (新) は oneof フィールドを通常のメッセージフィールドのように扱います。

Open Struct API (旧)Opaque API (新)
m := &pb.Foo{
  Uint32: myScalar,  // could be nil
  Union:  &pb.Foo_String{myString},
  Kind:   pb.Foo_SPECIAL_KIND.Enum(),
}
m := pb.Foo_builder{
  Uint32: myScalar,
  String: myString,
  Kind:   pb.Foo_SPECIAL_KIND.Enum(),
}.Build()

oneof ユニオンに関連付けられた Go 構造体フィールドのセットでは、1 つのフィールドのみを格納できます。複数の oneof ケースフィールドが格納されている場合、最後に宣言されたフィールド(.proto ファイル内のフィールド宣言順序)が優先されます。

スカラーフィールド

スカラーフィールドで定義されたメッセージがあるとします。

message Artist {
  int32 birth_year = 1;
}

Go がスカラー型 (bool, int32, int64, uint32, uint64, float32, float64, string, []byte, および enum) を使用する Protobuf メッセージフィールドには、Get および Set アクセサーメソッドがあります。明示的な存在を持つフィールドには、Has および Clear メソッドもあります。

birth_year という名前の int32 型のフィールドの場合、以下のアクセサーメソッドが生成されます。

func (m *Artist) GetBirthYear() int32
func (m *Artist) SetBirthYear(v int32)
func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()

Get はフィールドの値を返します。フィールドが設定されていないか、メッセージレシーバが nil の場合、デフォルト値を返します。デフォルト値は、デフォルトオプションで明示的に設定されていない限り、ゼロ値です。

Set は指定された値をフィールドに格納します。nil メッセージレシーバで呼び出された場合、パニックを起こします。

バイトフィールドの場合、nil []byte を指定して Set を呼び出すと、設定済みと見なされます。例えば、直後に Has を呼び出すと true を返します。直後に Get を呼び出すと、長さゼロのスライス(nil または空のスライスのいずれか)が返されます。ユーザーは、存在を判断するために Has を使用し、Get が nil を返すかどうかに依存すべきではありません。

Has はフィールドが格納されているかどうかを報告します。nil メッセージレシーバで呼び出された場合、false を返します。

Clear はフィールドをクリアします。nil メッセージレシーバで呼び出された場合、パニックを起こします。

文字列フィールドを使用したコードスニペットの例。

Open Struct API (旧)Opaque API (新)
// Getting the value.
s := m.GetBirthYear()

// Setting the field.
m.BirthYear = proto.Int32(1989)

// Check for presence.
if s.BirthYear != nil {  }

// Clearing the field.
m.BirthYear = nil
// Getting the field value.
s := m.GetBirthYear()

// Setting the field.
m.SetBirthYear(1989)

// Check for presence.
if m.HasBirthYear() {  }

// Clearing the field
m.ClearBirthYear()

メッセージフィールド

メッセージ型フィールドで定義されたメッセージがあるとします。

message Band {}

message Concert {
  Band headliner = 1;
}

メッセージ型の Protobuf メッセージフィールドには、GetSetHas、および Clear メソッドがあります。

headliner という名前のメッセージ型フィールドの場合、以下のアクセサーメソッドが生成されます。

func (m *Concert) GetHeadliner() *Band
func (m *Concert) SetHeadliner(*Band)
func (m *Concert) HasHeadliner() bool
func (m *Concert) ClearHeadliner()

Get はフィールドの値を返します。設定されていない場合、または nil メッセージレシーバで呼び出された場合、nil を返します。Get が nil を返すかどうかをチェックすることは、Has が false を返すかどうかをチェックすることと同じです。

Set は指定された値をフィールドに格納します。nil メッセージレシーバで呼び出された場合、パニックを起こします。nil ポインタで Set を呼び出すことは、Clear を呼び出すことと同じです。

Has はフィールドが格納されているかどうかを報告します。nil メッセージレシーバで呼び出された場合、false を返します。

Clear はフィールドをクリアします。nil メッセージレシーバで呼び出された場合、パニックを起こします。

コードスニペットの例。

Open Struct API (旧)Opaque (新)
// Getting the value.
b := m.GetHeadliner()

// Setting the field.
m.Headliner = &pb.Band{}

// Check for presence.
if s.Headliner != nil {  }

// Clearing the field.
m.Headliner = nil
// Getting the value.
s := m.GetHeadliner()

// Setting the field.
m.SetHeadliner(&pb.Band{})

// Check for presence.
if m.HasHeadliner() {  }

// Clearing the field
m.ClearHeadliner()

繰り返しフィールド

繰り返しメッセージ型フィールドで定義されたメッセージがあるとします。

message Concert {
  repeated Band support_acts = 2;
}

繰り返しフィールドには、GetSet メソッドがあります。

Get はフィールドの値を返します。フィールドが設定されていないか、メッセージレシーバが nil の場合、nil を返します。

Set は指定された値をフィールドに格納します。nil メッセージレシーバで呼び出された場合、パニックを起こします。Set は提供されたスライスヘッダーのコピーを格納します。スライスコンテンツの変更は、繰り返しフィールドで観測可能です。したがって、空のスライスで Set が呼び出された場合、直後に Get を呼び出すと、同じスライスが返されます。ワイヤまたはテキストマーシャリング出力の場合、渡された nil スライスは空のスライスと区別できません。

メッセージ Concertsupport_acts という名前の繰り返しメッセージ型フィールドの場合、以下のアクセサーメソッドが生成されます。

func (m *Concert) GetSupportActs() []*Band
func (m *Concert) SetSupportActs([]*Band)

コードスニペットの例。

Open Struct API (旧)Opaque API (新)
// Getting the entire repeated value.
v := m.GetSupportActs()

// Setting the field.
m.SupportActs = v

// Get an element in a repeated field.
e := m.SupportActs[i]

// Set an element in a repeated field.
m.SupportActs[i] = e

// Get the length of a repeated field.
n := len(m.GetSupportActs())

// Truncate a repeated field.
m.SupportActs = m.SupportActs[:i]

// Append to a repeated field.
m.SupportActs = append(m.GetSupportActs(), e)
m.SupportActs = append(m.GetSupportActs(), v...)

// Clearing the field.
m.SupportActs = nil
// Getting the entire repeated value.
v := m.GetSupportActs()

// Setting the field.
m.SetSupportActs(v)

// Get an element in a repeated field.
e := m.GetSupportActs()[i]

// Set an element in a repeated field.
m.GetSupportActs()[i] = e

// Get the length of a repeated field.
n := len(m.GetSupportActs())

// Truncate a repeated field.
m.SetSupportActs(m.GetSupportActs()[:i])

// Append to a repeated field.
m.SetSupportActs(append(m.GetSupportActs(), e))
m.SetSupportActs(append(m.GetSupportActs(), v...))

// Clearing the field.
m.SetSupportActs(nil)

マップ

マップ型フィールドで定義されたメッセージがあるとします。

message MerchBooth {
  map<string, MerchItems> items = 1;
}

マップフィールドには、GetSet メソッドがあります。

Get はフィールドの値を返します。フィールドが設定されていないか、メッセージレシーバが nil の場合、nil を返します。

Set は指定された値をフィールドに格納します。nil メッセージレシーバで呼び出された場合、パニックを起こします。Set は提供されたマップ参照のコピーを格納します。提供されたマップへの変更は、マップフィールドで観測可能です。

メッセージ MerchBoothitems という名前のマップフィールドの場合、以下のアクセサーメソッドが生成されます。

func (m *MerchBooth) GetItems() map[string]*MerchItem
func (m *MerchBooth) SetItems(map[string]*MerchItem)

コードスニペットの例。

Open Struct API (旧)Opaque API (新)
// Getting the entire map value.
v := m.GetItems()

// Setting the field.
m.Items = v

// Get an element in a map field.
v := m.Items[k]

// Set an element in a map field.
// This will panic if m.Items is nil.
// You should check m.Items for nil
// before doing the assignment to ensure
// it won't panic.
m.Items[k] = v

// Delete an element in a map field.
delete(m.Items, k)

// Get the size of a map field.
n := len(m.GetItems())

// Clearing the field.
m.Items = nil
// Getting the entire map value.
v := m.GetItems()

// Setting the field.
m.SetItems(v)

// Get an element in a map field.
v := m.GetItems()[k]

// Set an element in a map field.
// This will panic if m.GetItems() is nil.
// You should check m.GetItems() for nil
// before doing the assignment to ensure
// it won't panic.
m.GetItems()[k] = v

// Delete an element in a map field.
delete(m.GetItems(), k)

// Get the size of a map field.
n := len(m.GetItems())

// Clearing the field.
m.SetItems(nil)

Oneof

各 oneof ユニオングループには、メッセージに WhichHas、および Clear メソッドがあります。また、そのユニオン内の各 oneof ケースフィールドには、GetSetHas、および Clear メソッドがあります。

avatar oneof に image_url および image_data oneof フィールドを持つメッセージが次のように定義されているとします。

message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

この oneof に対して生成される Opaque API は次のようになります。

func (m *Profile) WhichAvatar() case_Profile_Avatar {  }
func (m *Profile) HasAvatar() bool {  }
func (m *Profile) ClearAvatar() {  }

type case_Profile_Avatar protoreflect.FieldNumber

const (
  Profile_Avatar_not_set_case case_Profile_Avatar = 0
  Profile_ImageUrl_case case_Profile_Avatar = 1
  Profile_ImageData_case case_Profile_Avatar = 2
)

Which は、フィールド番号を返すことで、どのケースフィールドが設定されているかを報告します。何も設定されていない場合、または nil メッセージレシーバで呼び出された場合、0 を返します。

Has は、oneof 内のいずれかのフィールドが設定されているかどうかを報告します。nil メッセージレシーバで呼び出された場合、false を返します。

Clear は、oneof で現在設定されているケースフィールドをクリアします。nil メッセージレシーバで呼び出された場合、パニックを起こします。

各 oneof ケースフィールドに対して生成される Opaque API は次のようになります。

func (m *Profile) GetImageUrl() string {  }
func (m *Profile) GetImageData() []byte {  }

func (m *Profile) SetImageUrl(v string) {  }
func (m *Profile) SetImageData(v []byte) {  }

func (m *Profile) HasImageUrl() bool {  }
func (m *Profile) HasImageData() bool {  }

func (m *Profile) ClearImageUrl() {  }
func (m *Profile) ClearImageData() {  }

Get はケースフィールドの値を返します。ケースフィールドが設定されていない場合、または nil メッセージレシーバで呼び出された場合、ゼロ値を返します。

Set は指定された値をケースフィールドに格納します。また、以前に oneof ユニオン内で格納されていたケースフィールドも暗黙的にクリアします。oneof メッセージケースフィールドで nil 値を指定して Set を呼び出すと、フィールドは空のメッセージに設定されます。nil メッセージレシーバで呼び出された場合、パニックを起こします。

Has は、ケースフィールドが設定されているかどうかを報告します。nil メッセージレシーバで呼び出された場合、false を返します。

Clear はケースフィールドをクリアします。以前に設定されていた場合、oneof ユニオンもクリアされます。oneof ユニオンが異なるフィールドに設定されている場合、oneof ユニオンはクリアされません。nil メッセージレシーバで呼び出された場合、パニックを起こします。

コードスニペットの例。

Open Struct API (旧)Opaque API (新)
// Getting the oneof field that is set.
switch m.GetAvatar().(type) {
case *pb.Profile_ImageUrl:
   = m.GetImageUrl()
case *pb.Profile_ImageData:
   = m.GetImageData()
}

// Setting the fields.
m.Avatar = &pb.Profile_ImageUrl{"http://"}
m.Avatar = &pb.Profile_ImageData{img}

// Checking whether any oneof field is set
if m.Avatar != nil {  }

// Clearing the field.
m.Avatar = nil

// Checking if a specific field is set.
_, ok := m.GetAvatar().(*pb.Profile_ImageUrl)
if ok {  }

// Clearing a specific field
_, ok := m.GetAvatar().(*pb.Profile_ImageUrl)
if ok {
  m.Avatar = nil
}

// Copy a oneof field.
m.Avatar = src.Avatar
// Getting the oneof field that is set.
switch m.WhichAvatar() {
case pb.Profile_ImageUrl_case:
   = m.GetImageUrl()
case pb.Profile_ImageData_case:
   = m.GetImageData()
}

// Setting the fields.
m.SetImageUrl("http://")
m.SetImageData([]byte("…"))

// Checking whether any oneof field is set
if m.HasAvatar() {  }

// Clearing the field.
m.ClearAvatar()

// Checking if a specific field is set.
if m.HasImageUrl() {  }

// Clearing a specific field.
m.ClearImageUrl()

// Copy a oneof field
switch src.WhichAvatar() {
case pb.Profile_ImageUrl_case:
  m.SetImageUrl(src.GetImageUrl())
case pb.Profile_ImageData_case:
  m.SetImageData(src.GetImageData())
}

リフレクション

Go の reflect パッケージを proto メッセージ型で使用して構造体フィールドやタグにアクセスするコードは、Open Struct API から移行すると機能しなくなります。コードは protoreflect を使用するように移行する必要があります。

一般的なライブラリの中には、内部で Go の reflect を使用しているものがあります。例を次に示します。