Новый API Go для Protocol Buffers

Автор оригинала: Joe Tsai, Damien Neil, and Herbie Ong
  • Перевод
В преддверии старта курса «Разработчик Golang» подготовили для вас перевод статьи из официального блога Golang.




Введение


Мы рады объявить вам о релизе major ревизии API Go для protocol buffers — формата обмена данными Google, не зависящего от языка.

Предпосылки для обновления API


Первые биндинги protocol buffer для Go были представлены Робом Пайком в марте 2010 года. Go 1 не будет выпущен еще два года.

За десять лет, прошедших с момента первого релиза, пакет рос и развивался вместе с Go. Выросли и запросы его пользователей.

Многие люди хотят писать программы, используя рефлексию для работы с сообщениями protocol buffer. Пакет reflect дает возможность просматривать типы и значения Go, но упускает информацию системы типов protocol buffer. Например, нам могло бы понадобиться написать функцию, которая просматривает весь лог целиком и очищает любое поле, аннотированное как содержащее конфиденциальные данные. Аннотации не являются частью системы типов Go.

Другая распространенная потребность — использовать структуры данных, отличные от тех, которые генерируются компилятором protocol buffer, такие как, например, динамический тип сообщения, способный представлять сообщения, тип которых неизвестен во время компиляции.

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

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

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

Сегодня мы рады выпустить этот новый модуль. Мы надеемся, что он Вам понравится.

Рефлексия


Рефлексия — это флагманская фича новой реализации. Подобно тому, как пакет reflect предоставляет просмотр типов и значений Go, пакет google.golang.org/protobuf/reflect/protoreflect предоставляет просмотр значений в соответствии с системой типов protocol buffer.

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

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

syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
    bool non_sensitive = 50000;
}

Мы можем использовать эту опцию, чтобы отметить определенные поля как non-sensitive (не содержащие конфиденциальные данные).

message MyMessage {
    string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}

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

// Redact очищает каждое конфиденциальное поле в pb.
func Redact(pb proto.Message) {
   // ...
}

Эта функция принимает proto.Message — интерфейс, реализуемый всеми генерируемыми типами сообщений. Этот тип является псевдонимом для типа определенного в пакете protoreflect:

type ProtoMessage interface{
    ProtoReflect() Message
}

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

(Почему псевдоним? Поскольку у protoreflect.Message есть соответствующий метод, возвращающий исходный proto.Message, и нам нужно избегать import cycle между двумя пакетами.)

Метод protoreflect.Message.Range вызывает функцию для каждого заполненного поля в сообщение.

m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
    // ...
    return true
})

Функция range вызывается с protoreflect.FieldDescriptor, описывающим тип буфера протокола поля и protoreflect.Value, содержащий значение поля.

Метод protoreflect.FieldDescriptor.Options возвращает опции поля в виде сообщения google.protobuf.FieldOptions.

opts := fd.Options().(*descriptorpb.FieldOptions)

(Зачем тут утверждение типа? Поскольку сгенерированный пакет descriptorpb зависит от protoreflect, пакет protoreflect не может вернуть конкретный тип опции, не вызывая import cycle.)

Затем мы можем проверить опции, чтобы увидеть значение логической переменной нашего расширения:

if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
    return true // не редактировать non-sensitive поля
}

Обратите внимание, что здесь мы смотрим на дескриптор поля, а не значение поля. Интересующая нас информация относится к системе типов protocol buffer, а не Go.

Это также пример области, в которой мы упростили API proto пакета. Исходный proto.GetExtension возвращал как значение, так и ошибку. Новый proto.GetExtension возвращает только значение, возвращая значение по умолчанию для поля, если оно отсутствует. Ошибки декодирования расширения сообщаются в Unmarshal.

После того, как мы определили поле, которое требует редактирования, очистить его достаточно просто:

m.Clear(fd)

Собрав все вышеперечисленное вместе, наша функция редактирования будет выглядеть так:

// Redact очищает каждое конфиденциальное поле в pb.
func Redact(pb proto.Message) {
    m := pb.ProtoReflect()
    m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
        opts := fd.Options().(*descriptorpb.FieldOptions)
        if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
            return true
        }
        m.Clear(fd)
        return true
    })
}

Более совершенная версия могла бы рекурсивно спускаться в поля с значениями сообщений. Мы надеемся, что этот простой пример дает представление о рефлексии в protocol buffer и ее использовании.

Версии


Мы называем исходную версию protocol buffers Go APIv1, а новую — APIv2. Поскольку APIv2 не имеет обратной совместимости с APIv1, нам необходимо использовать разные пути к модулям для каждого.

(Эти версии API не то же, что версии языка protocol buffer: proto1, proto2, и proto3. APIv1 и APIv2 — это конкретные реализации в Go, которые поддерживают как версию языка proto2, так и proto3)

В модуле github.com/golang/protobuf — APIv1.

В модуле google.golang.org/protobuf — APIv2. Мы воспользовались необходимостью изменить путь импорта, чтобы переключиться на тот, который не привязан к конкретному хостинг-провайдеру. (Мы рассматривали google.golang.org/protobuf/v2, чтобы было понятнее, что это вторая major версия API, но остановились на более коротком пути в качестве лучшего выбора в долгосрочной перспективе.)

Мы понимаем, что не все пользователи будут переходить на новую major версию пакета с одинаковой скоростью. Некоторые переключатся быстро; другие могут оставаться на старой версии неопределенно долгий срок. Даже в рамках одной программы некоторые части могут использовать один API, а другие — другой. Поэтому важно, чтобы мы продолжали поддерживать программы, использующие APIv1.

  • github.com/golang/protobuf@v1.3.4 — это самая последняя версия APIv1 до APIv2.
  • github.com/golang/protobuf@v1.4.0 — это версия APIv1, реализованная на основе APIv2. API тот же, но базовая реализация поддерживается новым API. Эта версия содержит функции для преобразования между интерфейсами proto.Message APIv1 и APIv2, чтобы облегчить переход между ними.
  • google.golang.org/protobuf@v1.20.0 — это APIv2. Этот модуль зависит от github.com/golang/protobuf@v1.4.0, поэтому любая программа, использующая APIv2, автоматически подхватит версию APIv1, которая интегрируется с ней.

(Почему мы начали с версии v1.20.0? Для ясности. Мы не ожидаем, что APIv1 когда-либо достигнет v1.20.0, поэтому одного номера версии должно быть достаточно, чтобы однозначно различать APIv1 и APIv2.)

Мы намерены дальше поддерживать APIv1, не устанавливая никаких сроков.

Такая организация гарантирует, что любая программа будет использовать только одну реализацию protocol buffer, независимо от того, какую версию API она использует. Это позволяет программам внедрять новый API постепенно или не внедрять вовсе, при этом сохраняя преимущества новой реализации. Принцип выбора минимальной версии означает, что программы могут оставаться в старой реализации до тех пор, пока мейнтейнеры не решат обновить ее до новой (напрямую или путем обновления зависимостей).

Дополнительные фичи, на которые следует обратить внимание


Пакет google.golang.org/protobuf/encoding/protojson преобразует сообщения protocol buffer в JSON и обратно с помощью канонического сопоставления JSON, а также исправляет ряд проблем со старым пакетом jsonpb, которые было трудно изменить, не вызывая новых проблем для существующих пользователей.

Пакет google.golang.org/protobuf/types/dynamicpb предоставляет реализацию proto.Message для сообщений, protocol buffer тип которых определяется во время выполнения.

Пакет google.golang.org/protobuf/testing/protocmp предоставляет функции для сравнения protocol buffer сообщений с пакетом github.com/google/cmp.

Пакет google.golang.org/protobuf/compiler/protogen предоставляет поддержку для написания плагинов компилятора protocol buffer.

Заключение


Модуль google.golang.org/protobuf — это серьезная переработка поддержки Go для protocol buffer, обеспечивающая первоклассную поддержку рефлексии, реализации пользовательских сообщений и очищенное от шероховатостей API. Мы намерены и дальше поддерживать предыдущий API в качестве оболочки нового, позволяя пользователям постепенно внедрять новый API в своем собственном темпе.

Наша цель в этом обновлении — усилить преимущества старого API и устранить его недостатки. По мере того, как мы завершали каждый компонент новой реализации, мы начинали использовать его в кодовой базе Google. Это постепенное развертывание вселило в нас уверенность как в удобстве использования нового API, так и в лучшей производительности и правильности новой реализации. Мы уверены, что оно готово к продакшену.

Мы очень рады этому выпуску и надеемся, что он хорошо послужит экосистеме Go в течение следующего десятка лет или даже дольше!



Узнать подробнее о курсе.


OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое