Проверка нулевого значения Enum в буфере протокола
2 марта 2022 г.- Как проверить, что сообщение Protobuf не содержит полей перечисления с нулевым значением? Оказывается, Protobuf не поддерживает его напрямую! Нам нужно изучить, как
protojson
пакет реализован.*
Все больше и больше компаний используют gRPC с Protobuf для связи между внутренними службами. Он обладает преимуществами высокой производительности, поддержки нескольких языков программирования и поддержки Google с отличной экосистемой.
Для связи с интерфейсными и внешними службами Protobuf можно маршалировать в формат JSON. Браузер понимает только формат JSON, и мы не можем ожидать, что другие компании будут использовать Protobuf напрямую от нас. (Конечно, вы можете, если вы достаточно большой!)
Давайте поговорим о перечислении и некоторых проблемах, с которыми мы недавно столкнулись.
Пример кода написан на Go.
Из Protobuf руководства по стилю перечисление с нулевым значением должно иметь суффикс UNSPECIFIED
. Это связано с тем, что enum реализован как uint32, а значение 0 считается неуказанным. Это похоже на nil
для сообщения или пустой строки. При кодировании Protobuf в формате JSON сообщение nil
, перечисление UNSPECIFIED
или пустая строка игнорируются.
Мы следовали этому соглашению, пока однажды не сделали этого.
При отправке сообщений внешних веб-перехватчиков мы решили не использовать 0
как UNSPECIFIED
. Одна из причин заключается в том, что мы используем EmitUnpopulated: true, чтобы убедиться, что все поля включены в представление JSON при отправке сообщений веб-перехватчиков внешним сторонам. И мы не хотим, чтобы это значение UNSPECIFIED
отображалось в сообщениях веб-перехватчиков, если мы каким-то образом забыли установить для поля перечисления значение 0. Модульные тесты не могут выявить все ошибки; мы, инженеры, это знаем.
Это вызывает много проблем, поэтому нам пришлось вернуться и сделать значение 0
как UNSPECIFIED
снова. Одна из проблем заключается в том, что это вынуждает использовать EmitUnpopulated: true везде! И есть места, где мы не хотим выделять все незаселенные поля. Например, вызов некоторых сторонних API. В некоторых сообщениях перечисления UNSPECIFIED
смешиваются с перечислениями, не относящимися к UNSPECIFIED
; нет способов отправить правильный формат с этим. Используйте EmitUnpopulated: true
, сторонние API не понимают UNSPECIFIED
; используйте EmitUnpopulated: false
, а некоторые обязательные поля с не-UNSPECIFIED
опущены. Конечно, все они могут быть удалены с помощью рефакторинга, но будет проще просто принудительно использовать `UNSPECIFIED
в начале.
Почему бы просто не проверить, что все поля перечисления не установлены в 0 в сообщениях веб-перехватчиков? Вы можете спросить.
Оказывается, в Protobuf 3 нет простых способов сделать это!
В Protobuf 2 есть обязательный параметр, предотвращающий удаление поля. Этот параметр был удален в Protobuf 3, поскольку он предотвращает рефакторинг для удаления полей. Если мы забудем обновить каждую службу, чтобы удалить это больше не используемое обязательное
поле, особенно в компании с несколькими командами, работающими вместе, сообщения будут непреднамеренно удалены. Лучше не требовать этого заранее. (подробнее)
В Protobuf 3 был интерфейс jsonpb.JSONPBMarshaler. Мы можем просто реализовать этот интерфейс для всех перечислений, чтобы возвращать ошибку при виде нулевого значения. Но опять удалили! Как протокол, мы должны максимально минимизировать настройку. В противном случае эту настройку придется реализовать и поддерживать на всех разных языках в разных командах!
Итак, как проверить нулевое значение в полях перечисления?
Нам нужно добраться до пакета отражения. В интерфейсе protoreflect.Message есть метод Range() для перебора каждого заполненного поля. Мы можем использовать этот метод, чтобы убедиться, что нет полей enum с нулями… О, подождите. Он перебирает только заполненные поля. Таким образом, он не обнаружит нулевое значение в перечислении!
Но функция protojson.Marshal() может по-прежнему выдавать незаполненные поля с опцией EmitUnpopulated. Как он это реализует? Погрузитесь в encoding/protojson
, там есть фрагмент кода для перебора незаполненных полей (source). Давайте украдем:
```иди
// unpopulatedFieldRanger оборачивает protoreflect.Message и изменяет его Range
// метод для дополнительного перебора незаполненных полей.
введите unpopulatedFieldRanger struct{ pref.Message }
func (m unpopulatedFieldRanger) Range (f func (pref.FieldDescriptor, pref.Value) bool) {
fds := m.Descriptor().Поля()
для я := 0; я < fds.Len(); я++ {
fd := fds.Get(i)
если m.Has(fd) || fd.ContainingOneof() != ноль {
continue // игнорировать заполненные поля и поля внутри oneof
v := m.Get(fd)
isProto2Scalar := fd.Syntax() == pref.Proto2 && fd.Default().IsValid()
isSingularMessage := fd.Cardinality() != pref.Repeated && fd.Message() != nil
если isProto2Scalar || isSingularMessage {
v = pref.Value{} // использовать недопустимое значение для выдачи null
если !f(fd, v) {
вернуть
m.Message.Range(f)
Приведенный выше код перебирает дополнительные поля, перебирая protoreflect.Message.Descriptor().Fields()
. Поля внутри полей oneof
пропускаются. Незаполненные сингулярные поля «сообщения» устанавливаются как «недействительные» (думайте об этом как «пустые» в сгенерированном JSON) перед отправкой во входную функцию.
Еще немного кода для написания, например реализация метода перемещения для перебора всех различных типов Protobuf: сообщения, массива (повторяется), динамического Struct и, конечно же, enum. Но это решаемо. И теперь я могу отдохнуть.
Спасибо за прочтение! Если у вас есть лучший способ сделать это, сообщите мне, подключившись к Twitter 👋
Также опубликовано в мой блог.
Оригинал