Вопросы и ответы

Список часто задаваемых вопросов о реализации протокольных буферов в Go, с ответами на каждый.

Версии

В чем разница между github.com/golang/protobuf и google.golang.org/protobuf?

Модуль github.com/golang/protobuf — это оригинальный API протокольных буферов для Go.

Модуль google.golang.org/protobuf — это обновленная версия этого API, разработанная для простоты, удобства использования и безопасности. Основные особенности обновленного API — поддержка рефлексии и разделение пользовательского API от базовой реализации.

Мы рекомендуем использовать google.golang.org/protobuf в новом коде.

Версия v1.4.0 и выше github.com/golang/protobuf оборачивает новую реализацию и позволяет программам постепенно переходить на новый API. Например, известные типы, определенные в github.com/golang/protobuf/ptypes, являются просто псевдонимами тех, что определены в новом модуле. Таким образом, google.golang.org/protobuf/types/known/emptypb и github.com/golang/protobuf/ptypes/empty могут использоваться взаимозаменяемо.

Что такое proto1, proto2, proto3 и editions?

Это редакции языка протокольных буферов. Это отличается от реализации protobuf на Go.

  • Editions — это новейший и рекомендуемый способ написания Protocol Buffers. Новые функции будут выпускаться как часть новых редакций. Для получения дополнительной информации см. Редакции Protocol Buffer.

  • proto3 — это устаревшая версия языка. Мы рекомендуем новому коду использовать редакции.

  • proto2 — это устаревшая версия языка. Несмотря на то, что она была заменена proto3 и editions, proto2 все еще полностью поддерживается.

  • proto1 — это устаревшая версия языка. Она никогда не выпускалась как проект с открытым исходным кодом.

Существует несколько различных типов Message. Какой следует использовать?

  • "google.golang.org/protobuf/proto".Message — это интерфейсный тип, реализуемый всеми сообщениями, сгенерированными текущей версией компилятора протокольных буферов. Функции, которые работают с произвольными сообщениями, такие как proto.Marshal или proto.Clone, принимают или возвращают этот тип.

  • "google.golang.org/protobuf/reflect/protoreflect".Message — это интерфейсный тип, описывающий представление сообщения через рефлексию.

    Вызовите метод ProtoReflect у proto.Message, чтобы получить protoreflect.Message.

  • "google.golang.org/protobuf/reflect/protoreflect".ProtoMessage — это псевдоним "google.golang.org/protobuf/proto".Message. Эти два типа взаимозаменяемы.

  • "github.com/golang/protobuf/proto".Message — это интерфейсный тип, определенный устаревшим API протокольных буферов для Go. Все сгенерированные типы сообщений реализуют этот интерфейс, но интерфейс не описывает поведение, ожидаемое от этих сообщений. Новому коду следует избегать использования этого типа.

Распространенные проблемы

"go install": working directory is not part of a module

В Go 1.15 и ниже вы установили переменную окружения GO111MODULE=on и запускаете команду go install вне каталога модуля. Установите GO111MODULE=auto или удалите переменную окружения.

В Go 1.16 и выше go install можно вызывать вне модуля, указав явную версию: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

constant -1 overflows protoimpl.EnforceVersion

Вы используете сгенерированный файл .pb.go, который требует более новой версии модуля "google.golang.org/protobuf".

Обновитесь до более новой версии с помощью:

go get -u google.golang.org/protobuf/proto

undefined: "github.com/golang/protobuf/proto".ProtoPackageIsVersion4

Вы используете сгенерированный файл .pb.go, который требует более новой версии модуля "github.com/golang/protobuf".

Обновитесь до более новой версии с помощью:

go get -u github.com/golang/protobuf/proto

Что такое конфликт пространства имен протокольного буфера?

Все объявления протокольных буферов, связанные в бинарный файл Go, вставляются в глобальный реестр.

Каждое объявление protobuf (например, перечисления, значения перечислений или сообщения) имеет абсолютное имя, которое представляет собой конкатенацию имени пакета с относительным именем объявления в исходном файле .proto (например, my.proto.package.MyMessage.NestedMessage). Язык protobuf предполагает, что все объявления универсально уникальны.

Если два объявления protobuf, связанные в бинарный файл Go, имеют одинаковое имя, то это приводит к конфликту пространства имен, и реестру невозможно правильно разрешить это объявление по имени. В зависимости от того, какая версия Go protobuf используется, это либо вызовет панику во время инициализации, либо тихо проигнорирует конфликт и приведет к потенциальной ошибке позже во время выполнения.

Как исправить конфликт пространства имен протокольного буфера?

Лучший способ исправить конфликт пространства имен зависит от причины, по которой он возникает.

Распространенные способы возникновения конфликтов пространств имен:

  • Вендорные .proto файлы. Когда один файл .proto генерируется в два или более пакетов Go и связывается в один бинарный файл Go, это вызывает конфликт для каждого объявления protobuf в сгенерированных пакетах Go. Обычно это происходит, когда файл .proto вендорится и из него генерируется пакет Go, или сам сгенерированный пакет Go вендорится. Пользователям следует избегать вендоринга и вместо этого зависеть от централизованного пакета Go для этого файла .proto.

    • Если файлом .proto владеет внешняя сторона и в нем отсутствует опция go_package, то вам следует согласовать с владельцем этого файла .proto указание централизованного пакета Go, от которого множество пользователей могут зависеть.
  • Отсутствующие или общие имена пакетов proto. Если файл .proto не указывает имя пакета или использует слишком общее имя пакета (например, "my_service"), то высока вероятность того, что объявления внутри этого файла будут конфликтовать с другими объявлениями elsewhere во вселенной. Мы рекомендуем, чтобы каждый файл .proto имел имя пакета, которое намеренно выбрано быть универсально уникальным (например, с префиксом названия компании).

{{% alert title="Предупреждение" color="warning" %}} Ретроспективное изменение имени пакета в файле .proto не является обратно совместимым для типов, используемых в качестве полей расширений, хранящихся в google.protobuf.Any, или для определений gRPC Service. {{% /alert %}}

Начиная с v1.26.0 модуля google.golang.org/protobuf, будет сообщаться о жесткой ошибке при запуске программы Go, в которую связано несколько конфликтующих имен protobuf. Хотя предпочтительнее, чтобы источник конфликта был исправлен, фатальную ошибку можно немедленно обойти одним из двух способов:

  1. Во время компиляции. Поведение по умолчанию для обработки конфликтов может быть указано во время компиляции с помощью переменной, инициализируемой компоновщиком: go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn"

  2. Во время выполнения программы. Поведение для обработки конфликтов при выполнении конкретного бинарного файла Go может быть установлено с помощью переменной окружения: GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main

Как использовать редакции протокольных буферов?

Чтобы использовать редакцию protobuf, вы должны указать редакцию в вашем файле .proto. Например, чтобы использовать редакцию 2023, добавьте следующее в начало вашего файла .proto:

edition = "2023";

Компилятор протокольных буферов затем сгенерирует код Go, совместимый с указанной редакцией. С редакциями вы также можете включать или отключать определенные функции для вашего файла .proto. Для получения дополнительной информации см. Редакции Protocol Buffer.

Как управлять поведением моего сгенерированного кода Go?

С редакциями вы можете управлять поведением сгенерированного кода Go, включая или отключая определенные функции в вашем файле .proto. Например, чтобы установить поведение API для вашей реализации, вы должны добавить следующее в ваш файл .proto:

edition = "2023";

option features.(pb.go).api_level = API_OPAQUE;

Когда api_level установлен в API_OPAQUE, код Go, сгенерированный компилятором протокольных буферов, скрывает поля структуры, так что к ним больше нельзя получить прямой доступ. Вместо этого создаются новые методы доступа для получения, установки или очистки поля.

Для полного списка доступных функций и их описаний см. Функции для редакций.

Почему reflect.DeepEqual ведет себя неожиданно с сообщениями protobuf?

Сгенерированные типы сообщений протокольных буферов включают внутреннее состояние, которое может различаться даже между эквивалентными сообщениями.

Кроме того, функция reflect.DeepEqual не знает о семантике сообщений протокольных буферов и может сообщать о различиях, которых не существует. Например, поле-отображение, содержащее nil-отображение, и содержащее не-nil отображение нулевой длины, семантически эквивалентны, но будут reported как неравные reflect.DeepEqual.

Используйте функцию proto.Equal для сравнения значений сообщений.

В тестах вы также можете использовать пакет "github.com/google/go-cmp/cmp" с опцией protocmp.Transform(). Пакет cmp может сравнивать произвольные структуры данных, и cmp.Diff выдает читаемые человеком отчеты о различиях между значениями.

if diff := cmp.Diff(a, b, protocmp.Transform()); diff != "" {
  t.Errorf("unexpected difference:\n%v", diff)
}

Закон Хайрума

Что такое Закон Хайрума и почему он в этом FAQ?

Закон Хайрума гласит:

При достаточном количестве пользователей API не имеет значения, что вы обещаете в контракте: все наблюдаемые поведения вашей системы будут использоваться кем-то.

Целью дизайна последней версии API протокольных буферов для Go является избежание, там где это возможно, предоставления наблюдаемых поведений, которые мы не можем обещать сохранять стабильными в будущем. Наша философия заключается в том, что преднамеренная нестабильность в областях, где мы не даем обещаний, лучше, чем создание иллюзии стабильности, только для того, чтобы это изменилось в будущем после того, как проект потенциально долго полагался на это ложное предположение.

Почему текст ошибок постоянно меняется?

Тесты, зависящие от точного текста ошибок, хрупки и часто ломаются, когда этот текст изменяется. Чтобы препятствовать небезопасному использованию текста ошибок в тестах, текст ошибок, производимых этим модулем, намеренно нестабилен.

Если вам нужно идентифицировать, произведена ли ошибка protobuf модулем, мы гарантируем, что все ошибки будут соответствовать proto.Error согласно errors.Is.

Почему вывод protojson постоянно меняется?

Мы не даем обещаний о долгосрочной стабильности реализации Go JSON формата для протокольных буферов. Спецификация определяет только то, что является допустимым JSON, но не предоставляет спецификации для канонического формата того, как маршалер должен форматировать данное сообщение. Чтобы избежать создания иллюзии, что вывод стабилен, мы намеренно вносим незначительные различия, чтобы сравнения побайтово скорее всего завершались неудачей.

Чтобы получить некоторую степень стабильности вывода, мы рекомендуем пропускать вывод через форматтер JSON.

Почему вывод prototext постоянно меняется?

Мы не даем обещаний о долгосрочной стабильности реализации Go текстового формата. Не существует канонической спецификации текстового формата protobuf, и мы хотим сохранить возможность вносить улучшения в вывод пакета prototext в будущем. Поскольку мы не обещаем стабильности вывода пакета, мы намеренно внесли нестабильность, чтобы препятствовать пользователям зависеть от него.

Чтобы получить некоторую степень стабильности, мы рекомендуем пропускать вывод prototext через программу txtpbfmt. Форматтер может быть напрямую вызван в Go с помощью parser.Format.

Разное

Как использовать сообщение протокольного буфера в качестве хэш-ключа?

Вам нужна каноническая сериализация, где маршализованный вывод сообщения протокольного буфера гарантированно стабилен с течением времени. К сожалению, на данный момент не существует спецификации для канонической сериализации. Вам нужно будет написать свою собственную или найти способ обойтись без нее.

Могу ли я добавить новую функцию в реализацию протокольных буферов на Go?

Возможно. Мы всегда рады предложениям, но мы очень осторожны в добавлении новых вещей.

Реализация протокольных буферов на Go стремится быть последовательной с другими реализациями на языках. Как таковая, мы склонны избегать функций, которые чрезмерно специализированы только для Go. Специфичные для Go функции препятствуют цели протокольных буферов быть языково-нейтральным форматом обмена данными.

Если ваша идея не специфична для реализации на Go, вам следует присоединиться к группе обсуждения protobuf и предложить ее там.

Если у вас есть идея для реализации на Go, создайте issue в нашем трекере: https://github.com/golang/protobuf/issues

Могу ли я добавить опцию в Marshal или Unmarshal для их настройки?

Только если эта опция существует в других реализациях (например, C++, Java). Кодирование протокольных буферов (бинарная, JSON и текст) должно быть согласованным между реализациями, чтобы программа, написанная на одном языке, могла читать сообщения, написанные на другом.

Мы не будем добавлять какие-либо опции в реализацию на Go, которые влияют на данные, выводимые функциями Marshal или читаемые функциями Unmarshal, если эквивалентная опция не существует по крайней мере в одной другой поддерживаемой реализации.

Могу ли я настроить код, генерируемый protoc-gen-go?

В общем, нет. Protocol buffers предназначены быть языково-независимым форматом обмена данными, а специфичные для реализации настройки противоречат этому намерению.