Opaque API: Ручная миграция

Описывает ручную миграцию на Opaque API

Opaque API — это последняя версия реализации Protocol Buffers для языка программирования Go. Старая версия теперь называется Open Struct API. Смотрите Go Protobuf: Releasing the Opaque API (блогпост) для введения.

Это руководство пользователя по миграции использования Go Protobuf со старого Open Struct API на новый Opaque API.

{{% alert title="Предупреждение" color="warning" %}} Вы смотрите руководство по ручной миграции. Обычно лучше использовать инструмент open2opaque для автоматизации миграции. Смотрите Миграция на Opaque API вместо этого. {{% /alert %}}

Руководство по сгенерированному коду предоставляет более подробную информацию. Это руководство сравнивает старый и новый 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()

Как вы можете видеть, структуры builder позволяют осуществить почти 1:1 перевод между Open Struct API (старый) и Opaque API (новый).

В целом, предпочтительнее использовать builders для удобочитаемости. Только в редких случаях, например, при создании сообщений Protobuf в горячем внутреннем цикле, может быть предпочтительнее использовать сеттеры вместо builders. Смотрите Opaque API FAQ: Следует ли использовать builders или сеттеры? для более подробной информации.

Исключением из приведенного выше примера является работа с oneof: Open Struct API (старый) использует тип структуры-обертки для каждого случая oneof, тогда как Opaque API (новый) рассматривает поля oneof как обычные поля сообщения:

Open Struct API (старый) Opaque API (новый)
m := &pb.Foo{
  Uint32: myScalar,  // может быть nil
  Union:  &pb.Foo_String_{String_: myString},
  Kind:   pb.Foo_SPECIAL_KIND.Enum(),
}
m := pb.Foo_builder{
  Uint32: myScalar,
  String: myString,
  Kind:   pb.Foo_SPECIAL_KIND.Enum(),
}.Build()

Для набора полей структуры Go, связанных с объединением oneof, только одно поле может быть заполнено. Если заполнено несколько полей случаев oneof, побеждает последнее (в порядке объявления полей в вашем .proto файле).

Скалярные поля

Предположим, есть сообщение, определенное со скалярным полем:

message Artist {
  int32 birth_year = 1;
}

Поля сообщений Protobuf, для которых Go использует скалярные типы (bool, int32, int64, uint32, uint64, float32, float64, string, []byte и enum), будут иметь методы доступа Get и Set. Поля с явным присутствием также будут иметь методы Has и Clear.

Для поля типа int32 с именем birth_year будут сгенерированы следующие методы доступа для него:

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

Get возвращает значение для поля. Если поле не установлено или получатель сообщения равен nil, он возвращает значение по умолчанию. Значение по умолчанию — нулевое значение, если явно не установлено с помощью опции default.

Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil получателе сообщения.

Для полей bytes вызов Set с nil []byte будет считаться установленным. Например, вызов Has сразу после возвращает true. Вызов Get сразу после вернет срез нулевой длины (может быть либо nil, либо пустым срезом). Пользователи должны использовать Has для определения присутствия и не полагаться на то, возвращает ли Get nil.

Has сообщает, заполнено ли поле. Он возвращает false при вызове на nil получателе сообщения.

Clear очищает поле. Он вызывает панику при вызове на nil получателе сообщения.

Примеры фрагментов кода, использующих поле string в:

Open Struct API (старый) Opaque API (новый)
// Получение значения.
s := m.GetBirthYear()

// Установка поля.
m.BirthYear = proto.Int32(1989)

// Проверка присутствия.
if m.BirthYear != nil { … }

// Очистка поля.
m.BirthYear = nil
// Получение значения поля.
s := m.GetBirthYear()

// Установка поля.
m.SetBirthYear(1989)

// Проверка присутствия.
if m.HasBirthYear() { … }

// Очистка поля
m.ClearBirthYear()

Поля сообщений

Предположим, есть сообщение, определенное с полем типа сообщение:

message Band {}

message Concert {
  Band headliner = 1;
}

Поля сообщений Protobuf типа message будут иметь методы Get, Set, Has и Clear.

Для поля типа message с именем 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 получателе сообщения. Вызов Set с nil указателем эквивалентен вызову Clear.

Has сообщает, заполнено ли поле. Он возвращает false при вызове на nil получателе сообщения.

Clear очищает поле. Он вызывает панику при вызове на nil получателе сообщения.

Примеры фрагментов кода

Open Struct API (старый) Opaque (новый)
// Получение значения.
b := m.GetHeadliner()

// Установка поля.
m.Headliner = &pb.Band{}

// Проверка присутствия.
if m.Headliner != nil { … }

// Очистка поля.
m.Headliner = nil
// Получение значения.
s := m.GetHeadliner()

// Установка поля.
m.SetHeadliner(&pb.Band{})

// Проверка присутствия.
if m.HasHeadliner() { … }

// Очистка поля
m.ClearHeadliner()

Повторяющиеся поля

Предположим, есть сообщение, определенное с повторяющимся полем типа сообщение:

message Concert {
  repeated Band support_acts = 2;
}

Повторяющиеся поля будут иметь методы Get и Set.

Get возвращает значение для поля. Он возвращает nil, если поле не установлено или получатель сообщения равен nil.

Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil получателе сообщения. Set сохранит копию заголовка среза, который предоставлен. Изменения в содержимом среза наблюдаемы в повторяющемся поле. Следовательно, если Set вызывается с пустым срезом, вызов Get сразу после вернет тот же срез. Для вывода wire или text маршалинга переданный nil срез неотличим от пустого среза.

Для повторяющегося поля типа message с именем support_acts в сообщении Concert, будут сгенерированы следующие методы доступа для него:

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

Примеры фрагментов кода

Open Struct API (старый) Opaque API (новый)
// Получение всего повторяющегося значения.
v := m.GetSupportActs()

// Установка поля.
m.SupportActs = v

// Получить элемент в повторяющемся поле.
e := m.SupportActs[i]

// Установить элемент в повторяющемся поле.
m.SupportActs[i] = e

// Получить длину повторяющегося поля.
n := len(m.GetSupportActs())

// Усечь повторяющееся поле.
m.SupportActs = m.SupportActs[:i]

// Добавить в повторяющееся поле.
m.SupportActs = append(m.GetSupportActs(), e)
m.SupportActs = append(m.GetSupportActs(), v...)

// Очистка поля.
m.SupportActs = nil
// Получение всего повторяющегося значения.
v := m.GetSupportActs()

// Установка поля.
m.SetSupportActs(v)

// Получить элемент в повторяющемся поле.
e := m.GetSupportActs()[i]

// Установить элемент в повторяющемся поле.
m.GetSupportActs()[i] = e

// Получить длину повторяющегося поля.
n := len(m.GetSupportActs())

// Усечь повторяющееся поле.
m.SetSupportActs(m.GetSupportActs()[:i])

// Добавить в повторяющееся поле.
m.SetSupportActs(append(m.GetSupportActs(), e))
m.SetSupportActs(append(m.GetSupportActs(), v...))

// Очистка поля.
m.SetSupportActs(nil)

Отображения

Предположим, есть сообщение, определенное с полем типа отображение:

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

Поля-отображения будут иметь методы Get и Set.

Get возвращает значение для поля. Он возвращает nil, если поле не установлено или получатель сообщения равен nil.

Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil получателе сообщения. Set сохранит копию предоставленной ссылки на отображение. Изменения в предоставленном отображении наблюдаемы в поле отображения.

Для поля-отображения с именем items в сообщении MerchBooth будут сгенерированы следующие методы доступа для него:

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

Примеры фрагментов кода

Open Struct API (старый) Opaque API (новый)
// Получение всего значения отображения.
v := m.GetItems()

// Установка поля.
m.Items = v

// Получить элемент в поле-отображении.
v := m.Items[k]

// Установить элемент в поле-отображении.
// Это вызовет панику, если m.Items равно nil.
// Вы должны проверить m.Items на nil
// перед присваиванием, чтобы убедиться,
// что паники не будет.
m.Items[k] = v

// Удалить элемент в поле-отображении.
delete(m.Items, k)

// Получить размер поля-отображения.
n := len(m.GetItems())

// Очистка поля.
m.Items = nil
// Получение всего значения отображения.
v := m.GetItems()

// Установка поля.
m.SetItems(v)

// Получить элемент в поле-отображении.
v := m.GetItems()[k]

// Установить элемент в поле-отображении.
// Это вызовет панику, если m.GetItems() равно nil.
// Вы должны проверить m.GetItems() на nil
// перед присваиванием, чтобы убедиться,
// что паники не будет.
m.GetItems()[k] = v

// Удалить элемент в поле-отображении.
delete(m.GetItems(), k)

// Получить размер поля-отображения.
n := len(m.GetItems())

// Очистка поля.
m.SetItems(nil)

Oneofs

Для каждой группы объединения oneof будут методы Which, Has и Clear в сообщении. Также будут методы Get, Set, Has и Clear для каждого поля случая oneof в этом объединении.

Предположим, есть сообщение, определенное с полями oneof image_url и image_data в oneof avatar следующим образом:

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

Сгенерированный Opaque API для этого oneof будет:

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 сообщает, какое поле случая установлено, возвращая номер поля. Он возвращает 0, когда ни одно не установлено или при вызове на nil получателе сообщения.

Has сообщает, установлено ли любое из полей внутри oneof. Он возвращает false при вызове на nil получателе сообщения.

Clear очищает текущее установленное поле случая в oneof. Он вызывает панику на nil получателе сообщения.

Сгенерированный Opaque API для каждого поля случая oneof будет:

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. Вызов Set на поле случая сообщения oneof с nil значением установит поле в пустое сообщение. Он вызывает панику при вызове на nil получателе сообщения.

Has сообщает, установлено ли поле случая или нет. Он возвращает false при вызове на nil получателе сообщения.

Clear очищает поле случая. Если оно было ранее установлено, объединение oneof также очищается. Если объединение oneof установлено на другое поле, оно не очистит объединение oneof. Он вызывает панику при вызове на nil получателе сообщения.

Примеры фрагментов кода

Open Struct API (старый) Opaque API (новый)
// Получение установленного поля oneof.
switch m.GetAvatar().(type) {
case *pb.Profile_ImageUrl:
  … = m.GetImageUrl()
case *pb.Profile_ImageData:
  … = m.GetImageData()
}

// Установка полей.
m.Avatar = &pb.Profile_ImageUrl_{ImageUrl_: "http://"}
m.Avatar = &pb.Profile_ImageData_{ImageData_: img}

// Проверка, установлено ли любое поле oneof
if m.Avatar != nil { … }

// Очистка поля.
m.Avatar = nil

// Проверка, установлено ли конкретное поле.
_, ok := m.GetAvatar().(*pb.Profile_ImageUrl_)
if ok { … }

// Очистка конкретного поля
_, ok := m.GetAvatar().(*pb.Profile_ImageUrl_)
if ok {
  m.Avatar = nil
}

// Копирование поля oneof.
m.Avatar = src.Avatar
// Получение установленного поля oneof.
switch m.WhichAvatar() {
case pb.Profile_ImageUrl_case:
  … = m.GetImageUrl()
case pb.Profile_ImageData_case:
  … = m.GetImageData()
}

// Установка полей.
m.SetImageUrl("http://")
m.SetImageData([]byte("…"))

// Проверка, установлено ли любое поле oneof
if m.HasAvatar() { … }

// Очистка поля.
m.ClearAvatar()

// Проверка, установлено ли конкретное поле.
if m.HasImageUrl() { … }

// Очистка конкретного поля.
m.ClearImageUrl()

// Копирование поля oneof
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 под капотом, примеры: