
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.
Программа не обязана компилироваться: генератор читает типы напрямую из
исходников (go-type-parser),
тела функций игнорируются, а сломанные объявления мешают генерации только
если на них ссылается обрабатываемый тип. Копирование исходников во
временную директорию больше не используется, все сообщения об ошибках
указывают на реальные файлы и строки.
Опции командной строки генератора:
-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. По умолчанию файлы парзятся, но методы в них игнорятся. Опция вызовет проблемы с генерацией, если генерёнными методами пользуются в этом же пакете.
Поддерживаемые типы
- Все целочисленные (знаковые и беззнаковые) типы определённого размера:
int8, int16, int32, int64, uint8, uint16, uint32, uint64.
- Для целочисленных типов неопределённого размера (
int и uint) необходимо явно указать формат в директиве/теге.
- Типы с плавающей запятой:
float32 и float64.
- Строки.
- Слайсы и массивы поддерживаемых типов (включая слайсы, структуры и указатели).
- Map с ключём и значением любого поддерживаемого типа.
bool (возможно указать значение для true и false, по умолчанию - байты 1 и 0 соответственно).
- Структуры с полями поддерживаемых типов. Сериализуются все публичные поля. Изменить дефолтный тип можно указанием тега.
- Указатели на поддерживаемые типы.
- Опциональные значения: указатели и
sql.Null* с тегом optional (подробнее в разделе «Опциональные значения»).
- Типы, определённые на базе поддерживаемых.
time.Time (подробнее в разделе «time.Time»).
Теги
Для поддержки типа нужно добавить непосредственно перед его определением директиву //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) использует длину - следует воспользоваться слайсом. Массивы следует рассматривать как структуры с нумерованными полями.
Промоушн тегов для обобщённых (generic) типов
Для обобщённых типов (generics) тег, указанный при использовании типа, «промоутится» к полям, типы которых используют параметры типа. Это позволяет задавать формат сериализации параметров типа при инстанцировании:
type Event[T any] struct {
Data T
Type EventType `iproto:"ber"`
}
type Pair[A, B any] struct {
First A
Second B
}
//adv:iproto:
type MyStruct struct {
// тег "u16" промоутится к полю Data (тип T = string),
// задавая длину строки в формате uint16
Simple Event[string] `iproto:"u16"`
// тег "u8" промоутится к полю Data элемента слайса
Slice []Event[string] `iproto:"u16,u8"`
// без тега: поля структуры используют свои дефолтные форматы
Complex Event[Ints]
// несколько параметров типа: теги распределяются слева направо
// "u8" → First (string), "i16" → Second (int32)
P Pair[string, int32] `iproto:"u8,i16"`
// составные типы потребляют несколько компонентов тега:
// "ber","u8" → First ([]string: длина слайса + длина строки), "i16" → Second
C Pair[[]string, int32] `iproto:"ber,u8,i16"`
// можно указать меньше тегов, чем максимально возможно:
// "u8" → First (string), Second (int32) использует формат по умолчанию
Partial Pair[string, int32] `iproto:"u8"`
}
Компоненты тега распределяются по полям с параметрами типа слева направо. Каждое поле потребляет столько компонентов, сколько требуется его конкретному типу (tagWidth): string — 1, []string — 2 (длина слайса + длина элемента), map[K]V — 1 + tagWidth(K) + tagWidth(V) и т.д. Допускается указать меньше компонентов, чем суммарный tagWidth всех полей-параметров — оставшиеся поля используют форматы по умолчанию.
Поля, конкретный тип которых не потребляет тегов (например, структуры с tagWidth = 0), пропускаются при распределении.
Стандартные типы в промоушне
Типы time.Time и sql.Null* распознаются при расчёте tagWidth и распределении тегов:
time.Time — tagWidth = 1 (тег формата: i64 или u32)
sql.Null[T] и legacy sql.NullXXX — tagWidth зависит от первого компонента тега:
- если первый компонент —
optional: tagWidth = 1 + tagWidth(внутреннего типа значения)
- иначе: tagWidth = 0, тип пропускается при распределении и сериализуется как обычная структура
type Pair[A, B any] struct {
First A
Second B
}
//adv:iproto:
type MyStruct struct {
// "optional","ber" → First (sql.Null[string]: optional + BER-длина строки)
// "i16" → Second (int32)
NullPair Pair[sql.Null[string], int32] `iproto:"optional,ber,i16"`
// частичное заполнение: "optional" → Data (sql.Null[int64])
// внутренний int64 использует формат по умолчанию (i64)
NullEvent Event[sql.Null[int64]] `iproto:"optional"`
// без "optional": sql.Null[string] пропускается (tagWidth = 0),
// "i16" промоутится к Second (int32), sql.Null — как обычная структура
NullSkip Pair[sql.Null[string], int32] `iproto:"i16"`
}
Для не-обобщённых (non-generic) структур указание тега является ошибкой. Если у обобщённой структуры при промоушне тег конфликтует с явно указанным тегом поля, выводится предупреждение и используется промоутнутый тег.
Внимание: в версиях до v0.0.14, тег на обобщённых структурах молча игнорировался. Если ваш код использовал теги на обобщённых типах (например, Event[string] \iproto:"u16"`), бинарный формат изменится: ранее тег не применялся и использовался формат по умолчанию (например, u32` для длины строки), теперь будет применён указанный тег. Проверьте совместимость бинарного представления с отправляющей/читающей стороной.
Для 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'"`
}
time.Time
Поддерживается сериализация time.Time в двух форматах, задаваемых тегом:
| Тег |
Формат |
Описание |
Диапазон |
i64 (по умолчанию) |
int64, наносекунды с Unix epoch |
time.Time.UnixNano() / time.Unix(0, n) |
1677-09-21T00:12:43Z — 2262-04-11T23:47:16Z |
u32 |
uint32, секунды с Unix epoch |
time.Time.Unix() / time.Unix(s, 0) |
1970-01-01T00:00:00Z — 2106-02-07T06:28:15Z |
Можно использовать как time.Time напрямую, так и через алиас:
type myTime = time.Time
//adv:iproto:
type MyStruct struct {
CreatedAt time.Time // по умолчанию i64 (наносекунды)
UpdatedAt time.Time `iproto:"i64"` // наносекунды (явно)
ExpiredAt time.Time `iproto:"u32"` // секунды
AliasTime myTime // алиасы поддерживаются
}
Опциональные значения (optional)
Указатели и типы sql.Null* можно использовать для передачи опциональных значений.
Для типов sql.Null* проверяется поле Valid: если true, значение присутствует; если false — отсутствует.
nil-указатели считаются отсутствующими.
Поддерживаемые типы:
- Указатели на поддерживаемые типы:
*int, *string, *MyStruct, ...
sql.Null[T] (generic)
sql.NullString, sql.NullInt64, sql.NullInt32, sql.NullInt16, sql.NullFloat64, sql.NullBool, sql.NullByte, sql.NullTime
При сериализации опциональных значений, перед значением передаётся байт присутствия:
0 — значение отсутствует (запись занимает 1 байт)
1 — значение присутствует, за ним следует закодированное представление (запись занимает 1 + длина значения байт)
//adv:iproto:
type MyStruct struct {
OptionalInt *int `iproto:"optional,ber"`
OptionalStr *string `iproto:"optional,u8"`
OptionalStruct *Ints `iproto:"optional"`
NullStr sql.NullString `iproto:"optional,ber"`
NullInt sql.NullInt64 `iproto:"optional"`
NullGeneric sql.Null[int64] `iproto:"optional"`
}
Тег optional используется совместно с тегом типа значения (через запятую). Без тега optional указатели и sql.Null* сериализуются по старым правилам (для обратной совместимости):
- Указатель без
optional — разыменовывается, nil вызовет панику
sql.Null* без optional — сериализуется как обычная структура (поля Value + Valid)
Предопределённые типы
Для сериализации простых типов можно воспользоваться пакетом iprototypes (github.com/adventures-team/go-iproto/types).
В нём определены типы:
- В формате little endian:
Uint8 (алиас Byte), Uint16, Uint32 (алиас Rune), Uint64, Int8, Int16, Int32, Int64, Float32, Float64, Bool.
- В формате 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 по вырезанию этого кода.
Первые два пункта влияют только на читаемость генерённого кода, на результате компиляции это не отразится.