iproto - библиотека для сериализации структур в формате протокола iproto.
[[TOC]]
Использование
Для сериализации используются методы MarshalIProto и UnmarshalIProto:
type Marshaler interface {
MarshalIProto([]byte) ([]byte, error)
}
type Unmarshaler interface {
UnmarshalIProto([]byte) ([]byte, error)
}
type MarshalerUnmarshaler interface {
Marshaler
Unmarshaler
}
MarshalIProto дописывает в переданный буфер сериализованное представление типа (при помощи append). При реализации сетевых протоколов следует предвыделять буфер с достаточной ёмкостью (cap) для избежания избыточных аллокаций.
UnmarshalIProto читает из буфера данные, и возвращает его необработанный хвост. При возврате ошибки допустимо возвращать nil вместо остатка буфера, т.к. дальнейшая обработка данных не последует.
iprotogen генерирует реализацию этих методов для пользовательских типов, помеченных служебным комментарием //adv:iproto::
// MyInt - my defined type
//
//adv:iproto:ber
type MyInt int
// MyStruct - my defined struct type
//
//adv:iproto:
type MyStruct {
ID uint32 // по дефолту, используется тот же тип, что указан в коде
Number uint64 `iproto:"ber"`
Data []int `iproto:"u16,i32"`
}
// block-level comment
//adv:iproto:
type (
MyStr string
//adv:iproto:"ber"
MyBER uint // переопределяет дефолтный комментарий
)
Комментарии должны иметь тот же формат, что и теги структур. Если комментарий присутствует у блока type, он действует на все типы, указанные в нём, для которых не указан отдельный комментарий.
Тип поля структуры можно указать тегом iproto. Если любое поле структуры имеет такой тег (включая поля, не поддерживаемые iprotogen, например _) - структура будет обработана генератором, даже если не указан комментарий //adv:iproto.
Директивы уровня файла (комментарии перед package без пробелов после //) копируются из исходного файла в генерируемый, это нужно для корректной поддержки build tags (см. опцию -tags).
Запуск генератора
Для подключения генератора к проекту (модулю) надо выполнить команду:
$ go get -tool github.com/adventures-team/go-iproto/cmd/iprotogen
Для использования генератора достаточно прописать в одном из файлов пакета:
//go:generate go tool iprotogen
и запустить команду go generate. Будет обработан весь пакет, а не только файл, содержащий строку go:generate.
Опции командной строки генератора:
-in (или $GOFILE для использования из go generate) - входной файл.
-tags - использовать сборочные теги (директива //go:build). Аналогично go -tags или golangci-lint --build-tags.
-only - обрабатывать только указанный файл. По умолчанию обрабатываются все файлы пакета. Так сделано, т.к. накладные расходы на загрузку пакета сравнимы с загрузкой одного файла.
-r - обрабатывать вложенные пакеты (в подпапках)
-tests - обрабатывать тесты (файлы *_tests.go). По умолчанию, тесты игнорируются.
Опции для отладки самого iprotogen:
-no-format - не обрабатывать результат генерации при помощи goimports. Может сгенерироваться код с избыточными импортами.
-stdout - распечатать результат генерации на stdout. По умолчанию, результаты сохраняются в файлах "$(orig_name)_generated.go".
-ignore-gen - не обрабатывать файлы, сгенерённые iprotogen. По умолчанию файлы парзятся, но методы в них игнорятся. Опция вызовет проблемы с генерацией, если генерёнными методами пользуются в этом же пакете.
-disable-strip - не использовать копирование исходника с обрезанием тел методов (копируется самый внешний модуль целиком, а не только один файл) во временную директорию. Повышает производительность, но могут возникнуть сложности при изменении обрабатываемых типов.
-keep-stripped - не удалять временную директорию.
Поддерживаемые типы
- Все целочисленные (знаковые и беззнаковые) типы определённого размера:
int8, int16, int32, int64, uint8, uint16, uint32, uint64.
- Для целочисленных типов неопределённого размера (
int и uint) необходимо явно указать формат в директиве/теге.
- Типы с плавающей запятой:
float32 и float64.
- Строки.
- Слайсы и массивы поддерживаемых типов (включая слайсы, структуры и указатели).
- Map с ключём и значением любого поддерживаемого типа.
bool (возможно указать значение для true и false, по умолчанию - байты 1 и 0 соответственно).
- Структуры с полями поддерживаемых типов. Сериализуются все публичные поля. Изменить дефолтный тип можно указанием тега.
- Указатели на поддерживаемые типы.
- Типы, определённые на базе поддерживаемых.
Теги
Для поддержки типа нужно добавить непосредственно перед его определением директиву //adv:iproto:
//adv:iproto:
type MyStruct struct {
В директиве также можно указать формат сериализации значения:
//adv:iproto:"ber"
type MyInt int
Аналогичным образом, можно указать формат для полей структур:
//adv:iproto:
type MyStruct struct {
ID int `iproto:"ber"`
Если для числового типа (включая целочисленные и типы с плавающей точкой) указан формат размера, не совпадающего с дефолтным, то в методах сериализации будут сгенерированы проверки на границы этих типов, при нарушении будет возвращена обёрнутая ошибка iproto.ErrOverflow.
| Тип iproto |
Дефолт для типов |
Описание |
-, skip |
|
Тип/поле не используется |
u8 |
uint8 |
aka byte |
u16 |
uint16 |
|
u32 |
uint32, float32, длина слайса |
|
u64 |
uint64, float64 |
|
i8 |
int8 |
|
i16 |
int16 |
|
i32 |
int32 |
|
i64 |
int64 |
|
ber |
|
perl pack 'w' |
Для слайсов и строк можно указать тип длины (по умолчанию, u32) и тип элемента. Для вложенных слайсов можно указать все типы длин и тип элемента через запятую. В этом примере длина слайса слайсов передаётся в формате ber, длина слайсов строк - u16, а длина каждой строки - u8:
type MyStruct struct {
NestedSlice [][]string `iproto:"ber,u16,u8"`
}
Для целочисленных типов, размер которых не фиксирован спецификацией (int и uint) указание тега обязательно:
//adv:iproto:"ber,u16"
type MySlice []uint
Для массивов длина не передаётся, если принимающая/передающая сторона (например, perl) использует длину - следует воспользоваться слайсом. Массивы следует рассматривать как структуры с нумерованными полями.
Для map можно указать тип длины, один тег для ключа и произвольное кол-во тегов для значения.
//adv:iproto:"ber,u8,u16,i32"
type MapStringInts map[string][]int
В этом примере длина map передаётся в формате ber, длина строки-ключа - в формате u8, количество интов в значении - u16, сами инты - в формате i32.
Map с пустыми значениями имеет такое же бинарное представление, как и слайс аналогичного типа (передаётся только ключи), и поддержка map (dict, hash и т.д.) на принимающей/передающей стороне не требуется:
//adv:iproto:"ber,u8"
type Strings []string
//adv:iproto:"ber,u8"
type StringSet map[string]struct{}
Булевские типы передаются в виде одного байта, отдельно можно задать значения для true (по умолчанию, 0x01) и false (0x00). Значение можно указать в числовом виде (в десятичном, восьмеричном с префиксом 0 или 0o, или шестнадцатеричном с префиксом 0x), либо в виде символа (в апострофах):
//adv:iproto:
type Bools struct {
Default bool // true - 1, false - 0
Custom bool `iproto:"true: 49, false: 48"` // true - 49, false - 48
CustomTrue bool `iproto:"true: 0xFF"` // true - 255, false - 0
CustomChar bool `iproto:"true: 't', false: 'f'"`
}
Предопределённые типы
Для сериализации простых типов можно воспользоваться пакетом iprototypes (github.com/adventures-team/go-iproto/types).
В нём определены типы:
- В формате little endian:
Uint8 (алиас Byte), Uint16, Uint32 (алиас Rune), Uint64, Int8, Int16, Int32, Int64, Float32, Float64.
- В формате BER
BER (uint64)
- С длиной в формате BER:
String, Bytes, Slice
При кодировании простых типов ошибку можно игнорировать, при декодировании - не стоит.
Пример:
buf, _ = iprototypes.String(str).MarshalIProto(buf)
Поддержка сериализации для произвольных типов
Для самостоятельной сериализации своих типов можно определить методы MarshalIProto и UnmarshalIProto.
Если определён только один из методов, то для всех типов, использующих данный тип (например, слайсы или структуры с полями этого типа), будет сгенерирован только этот метод. Так сделано для избежания ошибок симметричности (сериализовали
в своём формате, попытались десериализовать в дефолтном). Если зачем-то нужно определить только одну из операций, а другу генерить по дефолту, см. пример: tests/asymmetrical_test.go.
TODO
Обязательно:
- Декодирование слайсов интерфейсов
- Научиться парзить директивы типов, включённых в структуру (как встроенных, так и именованных) (см. addons.CreationTime)
BUGS:
- Если объявить несимметричные маршалеры, и встроить тип с симметричными (например, генерёнными), то будет генериться оба метода, при том, что надо генерить только один.
- Не генерить обёртку, вызывающую Marshal/Unmarshal встроенного поля, в случае тривиального встраивания (см. tp/test/addons_retry_generated_test.go)
- Вообще, продумать логику обработки встраивания, что разрешить, что запретить, и документировать это.
Q5:
- Не генерировать избыточные приведения типов для алиасов (не забываем про 1.23+ gotypesalias) стандартных типов. Нужно переделать имеющийся механизм (isInt... и т.д.) на этапе парзинга, по аналогии с поддержкой []byte
- Переменные для сокращения длинных конструкций
- Поддержка нескольких тегов для ключа map (только когда понадобится в продуктовом проекте)
- marshaler pointer receiver (оптимизация для больших структур)
- Переиспользование уже выделенных указателей/слайсов/мэпов (?)
- Для типов, реализующих "пустой" маршалер, по прежнему генерится вызов Marshal/Unmarshal. Проверить, что эти конструкции в асмовом коде отсутствуют (оптимизатор), чтобы не городить over-engineering по вырезанию этого кода.
Первые два пункта влияют только на читаемость генерённого кода, на результате компиляции это не отразится.