Обложка статьи «Как работать с protobuf в Go»

Как работать с protobuf в Go

E-commerce давно перестали быть сайтами с картинками — сегодня это огромные онлайн-платформы с множеством высоконагруженных сервисов. В Ozon порядка 60% сервисов — от инфраструктурных проектов до пользовательских — написано на Go, в IT-лаборатории компании сейчас одна из самых больших golang-команд России. Об инструментах разработки и трендах в развитии языка рассказывает Владимир Сердюков, ведущий разработчик группы «Личный кабинет» Ozon.

Владимир Сердюков

Владимир Сердюков, ведущий разработчик группы «Личный кабинет» Ozon

Как-то ко мне пришел тестировщик и показал тестовый смартфон, на котором было запущено наше приложение — но вместо текста и элементов интерфейса на нем был белый экран. Мы начали разбираться.

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

Почему эту проблему стоит обсуждать?

Страница сайта и экран мобильного приложения состоят из множества виджетов. Каждый из них — это контракт, то есть договоренность, в каком виде он придет от бэкенда и как его показывать на клиенте. Если в контракте будет ошибка, это может нарушить работоспособность целого приложения (на самом деле уже нет, но раньше могло).

Подобные проблемы сложно локализовать, и они не заметны на этапе тестирования. Как же тогда их решить? Я бы рекомендовал следующее:

  • использовать один контракт для разных платформ (в одном виде для desktop, приложения и т.д.);
  • хранить контракты в одном репозитории;
  • генерировать код под разные платформы (не писать вручную, забыть про копипасту).

proto 2 vs proto 3

Для работы с контрактами мы в Ozon используем protobuf — механизм, придуманный Google для сериализации структур данных. Чтобы из proto-файлов сгенерировать код, мы используем proto 3 с набором плагинов, один из которых — gogoproto, призванный упростить этот процесс и частично забороть особенности proto v3.

Предыдущая версия протокола (proto 2) позволяла реализовывать обязательные поля при помощи тега optional и задавать стандартное значение. Однако это приводило к проблемам портирования возможности генерации кода на другие языки программирования.

Это стало причиной появления proto 3. В новой версии протокола поменялось отношение к обязательным полям: все поля стали необязательные, а значения по умолчанию просто не отправляются. Кроме того, были исправлены enum, улучшен декодинг в json и внесены другие мелкие изменения.

Как сделать поля обязательными в proto 3

Чтобы сделать поля обязательными, можно использовать Well-known типы данных. Это может быть строка, булево значение, число, timestamp и тому подобное.

Если появляется такая строка, значит поле отправится вне зависимости от того, что вы в него записали. Однако сложные типы данных (proto-сообщение) все равно остаются необязательными. Тут на помощь приходит плагин gogoproto, где можно указать, что ваша структура не nullable.

Кроме того, можно сделать свой собственный форк jsonpb. Это библиотека, которая proto-сообщения генерирует в json. В нее добавлены три вида тегов — написав их в любом месте файла, можно не переживать, что сообщение не будет отправлено.

Как проверить, что новые изменения не ломают контракты?

Теперь понятно, как можно сделать поля обязательными, но что делать, если вдруг поле перестало быть обязательным или произошло что-то еще?

Для этого есть несколько инструментов для валидации proto-файлов, например buf.build и uber/prototool.

Оба инструмента работают по схожей схеме: чтобы запустить валидацию, создайте yaml-файл, в нем укажите конфигурацию, какие proto-файлы надо проверить, а какие нет. Также можно указать, какую ветку из репозитория брать. В обоих случаях работа идет только с git.

uber/protool пока не поддерживает apiv2, поэтому стоит приглядеться внимательнее к buf.build.

Что такое apiv2?

Поддержка работы с protobuf в Go впервые была анонсирована в 2010 году, первая версия Go вышла только спустя два года. С тех пор прошло много времени, изменились требования.

В итоге, несколько месяцев назад, 2 марта 2020 года, была анонсирована apiv2. Вот что изменилось по сравнению с прошлой версией:

  • появилась рефлексия для proto.Message;
  • канонический маппинг для json-полей;
  • улучшение производительности;
  • не получится использовать старые плагины и инструменты.

Давайте рассмотрим пример того, что теперь можно сделать из коробки. Есть api, возвращающее данные, которые мы не хотим показывать в логах (например, пароли пользователей, номера банковских карт и т.п.).

Для этого подключаем специальный пакет google.protobuf.FieldOptions и сразу добавляем флаг non_sensitive. Он будет отвечать за то, нужно ли маскировать данные внутри ответа. Выставим по умолчанию значение false (привет, proto2).

Дальше в функции-обработчике где-нибудь в середине формирования сообщения делаем следующее:

Подключаем новый интерфейс — ProtoReflect() и используем функцию Range().

Таким образом проходимся по все кастомным полям и проверяем значение non_sensitive поля (конечно же, используя рефлексию). Как только находим поле, в котором нужно убрать данные, вызываем Clear().

Таким нехитрым способом можно реализовать разные бизнес-кейсы.

Что в итоге?

  • В новых проектах используйте proto3.
  • apiv2 для Go не просто можно, но и нужно использовать.
  • Не забывайте валидировать свои proto-файлы.
  • Buf.build выглядит перспективно.
  • Плагины для генерации go-кода не актуальны, но можно увидеть много проектов, где они еще используются.

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Honor Cup, бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Перейти к регистрации