Go Opaque API: 手動移行

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

Opaque API は、Go プログラミング言語向けの Protocol Buffers 実装の最新バージョンです。古いバージョンは Open Struct API と呼ばれています。詳細は、Go Protobuf: 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: ビルダーとセッターのどちらを使うべきですか?をご覧ください。

上記の例の例外は、oneof を扱う場合です。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 メッセージレシーバで呼び出された場合、パニックになります。

bytes フィールドの場合、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 メッセージフィールドには、`Get`、`Set`、`Has`、および `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;
}

繰り返しフィールドには `Get` および `Set` メソッドがあります。

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

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

`Concert` メッセージの `support_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;
}

マップフィールドには `Get` および `Set` メソッドがあります。

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

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

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

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)

Oneofs

各 oneof 共用体グループには、メッセージに `Which`、`Has`、および `Clear` メソッドがあります。また、その共用体内の各 oneof ケースフィールドには、`Get`、`Set`、`Has`、および `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 共用体内で以前に格納されていたケースフィールドも暗黙的にクリアします。nil 値を持つ oneof メッセージケースフィールドで `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` を使用しています。例としては、