AccountBalance
и CurrencyAmount
, которые содержат информацию о балансе пользователя. В текущей версии у этих типов нет структурных тегов:// AccountBalance содержит анонимизированную информацию о балансе аккаунта.
type AccountBalance struct {
AccountIdHash []byte // хеш идентификатора пользователя
Amounts []CurrencyAmount // балансы в разных валютах
IsBlocked bool // флаг блокировки аккаунта
}
// CurrencyAmount содержит нормированное представление баланса.
// 1.50$ -> { Amount: 150, Decimals: 2, Symbol: "$" }.
type CurrencyAmount struct {
Amount int64 // нормальное значение баланса
Decimals int8 // количество цифр после запятой
Symbol string // идентификатор валюты
}
go-yaml
использует рефлексию для получения метаданных о типе. Чтобы задать опции для поля структуры, применяют структурные теги yaml
:type MyType struct {
ID int `yaml:"id"`
Title string `yaml:"title,omitempty"`
List []string `yaml:"name,flow"`
}
-
. omitempty
— этот флаг исключает поле из YAML-представления, если поле имеет нулевое значение (аналогично JSON, XML);flow
— при использовании этого флага для структур, слайсов и мап данные отобразятся в однострочном формате;inline
— при использовании этого флага для структур все их поля отобразятся как поля родительского объекта.flow
и inline
влияют на конечный результат, приведём такой пример: type Leaf struct {
ID int `yaml:"id"`
Name string `yaml:"name"`
}
type Tree struct {
Owner Leaf `yaml:"owner"`
Left Leaf `yaml:"left,flow"`
Right Leaf `yaml:"right,inline"`
}
var tree = Tree{
Owner: Leaf{1, "Owner"},
Left: Leaf{2, "Left child"},
Right: Leaf{3, "Right child"},
}
tree
в YAML-формате будет выглядеть так:owner:
id: 1
name: Owner
left: {id: 2, name: Left child}
id: 3
name: Right child
Marshal()
и Unmarshal()
. Также есть типы Encoder
и Decoder
с соответствующими методами Encode()
и Decode()
.yaml
для типов AccountBalance
, CurrencyAmount
и преобразуем переменную balance
в YAML-формат:package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
type (
AccountBalance struct {
AccountIdHash []byte `yaml:"account_id_hash,flow"`
Amounts []CurrencyAmount `yaml:"amounts,omitempty"`
IsBlocked bool `yaml:"is_blocked"`
}
CurrencyAmount struct {
Amount int64 `yaml:"amount"`
Decimals int8 `yaml:"decimals"`
Symbol string `yaml:"symbol"`
}
)
func main() {
balance := AccountBalance{
AccountIdHash: []byte{0x10, 0x20, 0x0A, 0x0B},
Amounts: []CurrencyAmount{
{Amount: 1000000, Decimals: 2, Symbol: "RUB"},
{Amount: 2510, Decimals: 2, Symbol: "USD"},
},
IsBlocked: true,
}
// преобразуем значение переменной balance в YAML-формат
out, err := yaml.Marshal(balance)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
account_id_hash: [16, 32, 10, 11]
amounts:
- amount: 1000000
decimals: 2
symbol: RUB
- amount: 2510
decimals: 2
symbol: USD
is_blocked: true
toml
, comment
и commented
:toml:"name,[omitempty]"
— определяет имя поля в TOML-представлении аналогично JSON, XML, YAML;comment:"Комментарий"
— добавляет комментарий с указанной строкой выше TOML-поля;commented:"true"
— комментирует поле в TOML-представлении.github.com/pelletier/go-toml
.toml
-теги.toml.Marshal(balance)
.package main
import (
"fmt"
"github.com/pelletier/go-toml"
)
type (
AccountBalance struct {
AccountIdHash []byte `toml:"account_id_hash"`
Amounts []CurrencyAmount `toml:"amounts,omitempty"`
IsBlocked bool `toml:"is_blocked" comment:"Deprecated" commented:"true"`
}
CurrencyAmount struct {
Amount int64 `toml:"amount"`
Decimals int8 `toml:"decimals"`
Symbol string `toml:"symbol"`
}
)
func main() {
balance := AccountBalance{
AccountIdHash: []byte{0x10, 0x20, 0x0A, 0x0B},
Amounts: []CurrencyAmount{
{Amount: 1000000, Decimals: 2, Symbol: "RUB"},
{Amount: 2510, Decimals: 2, Symbol: "USD"},
},
IsBlocked: true,
}
// преобразуем значение переменной balance в TOML-формат
out, err := toml.Marshal(balance)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
account_id_hash = [16, 32, 10, 11]
# Deprecated
# is_blocked = true
[[amounts]]
amount = 1000000
decimals = 2
symbol = "RUB"
[[amounts]]
amount = 2510
decimals = 2
symbol = "USD"
Marshal()
и Unmarshal()
.easyjson
(${GOPATH}/bin/easyjson
), которая генерирует код сериализации и десериализации конкретных типов:go get github.com/mailru/easyjson
go install github.com/mailru/easyjson/...@latest
easyjson <file.go>
с полным или относительным путём к файлу со структурами. В директории с исходным файлом будет создан file_easyjson.go
c нужными для конвертации методами, включая MarshalJSON()
и UnmarshalJSON()
. reflect
. easyjson
не работает для файлов в пакетах main
.easyjson
может принимать дополнительные параметры командной строки, например:easyjson -all -snake_case myjson.go
-all
генерирует код для всех типов в указанном файле;-snake_case
форматирует имена JSON-полей в свой формат.json.Marshaler
и json.Unmarshaler
. Объект можно использовать со стандартной библиотекой (с функциями json.MarshalIndent()
, json.Unmarshal()
и другими). Однако разработчик easyjson
не советует этого делать, так как easyjson.Marshal()
отработает быстрее, чем json.Marshal()
.easyjson
:// myeasyjson/myjson/myjson.go
// не забудьте запустить easyjson -all myjson.go
package myjson
type (
AccountBalance struct {
AccountIdHash []byte `json:"account_id_hash,flow"`
Amounts []CurrencyAmount `json:"amounts,omitempty"`
IsBlocked bool `json:"is_blocked"`
}
CurrencyAmount struct {
Amount int64 `json:"amount"`
Decimals int8 `json:"decimals"`
Symbol string `json:"symbol"`
}
)
// myeasyjson/main.go
package main
import (
"fmt"
"myeasyjson/myjson"
"github.com/mailru/easyjson"
)
func main() {
balance := myjson.AccountBalance{
AccountIdHash: []byte{0x10, 0x20, 0x0A, 0x0B},
Amounts: []myjson.CurrencyAmount{
{Amount: 1000000, Decimals: 2, Symbol: "RUB"},
{Amount: 2510, Decimals: 2, Symbol: "USD"},
},
IsBlocked: true,
}
// преобразуем значение переменной balance в JSON-формат
out, err := easyjson.Marshal(balance)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
{"account_id_hash":"ECAKCw==","amounts":[{"amount":1000000,"decimals":2,"symbol":"RUB"},{"amount":2510,"decimals":2,"symbol":"USD"}],"is_blocked":true}
easyjson
можно воспользоваться директивой //go:generate
и указать там запуск easyjson
с нужными параметрами:package myjson
//go:generate easyjson -all myjson.go
type AccountBalance struct {
AccountIdHash []byte `json:"account_id_hash,flow"`
Amounts []CurrencyAmount `json:"amounts,omitempty"`
IsBlocked bool `json:"is_blocked"`
}
//...
go generate
, которая и вызовет указанную в директиве программу.easyjson
подходит для случаев, когда схема данных известна заранее и не нужно производить динамическую десериализацию. Если же это необходимо, можно ограничить применение easyjson
для статических типов, а для динамической части реализовать JSON-интерфейсы самостоятельно.go install github.com/tinylib/msgp@latest
. //go:generate msgp
перед объявлением сериализуемых типов и запустить go generate
. Опционально можно указать структурные теги msg
:type S struct {
Name string `msg:"name"` // переопределение имени поля в представлении
Comment string `msg:"-"` // игнорирование поля
}
msgp
создаёт для типов файл <filename>_gen.go
с реализацией следующих методов: MarshalMsg()
, UnmarshalMsg()
, EncodeMsg()
, DecodeMsg()
, Msgsize()
. Рассмотрим подробнее методы для сериализации и десериализации объектов.MarshalMsg([]byte)([]byte, error)
MarshalMsg()
принимает на вход слайс байт и использует его для хранения результата. Если длины буфера недостаточно, создаётся новый слайс (в примере для создания нового буфера данных передаётся nil
).UnmarshalMsg([]byte)([]byte, error)
UnmarshalMsg()
принимает на вход слайс байт, содержащий сериализованные данные. Помимо ошибки, функция возвращает слайс байт с тем, что осталось после десериализации, то есть неиспользованные данные.AccountBalance
и CurrencyAmount
, то сериализация и десериализация будут выглядеть так:package main
import "fmt"
//go:generate msgp
type AccountBalance struct {
AccountIdHash []byte `msg:"account_id_hash"`
Amounts []CurrencyAmount `msg:"amounts"`
IsBlocked bool `msg:"is_blocked"`
}
type CurrencyAmount struct {
Amount int64 // здесь не будем использовать структурные теги
Decimals int8
Symbol string
}
func main() {
balance := AccountBalance{
AccountIdHash: []byte{0x10, 0x20, 0x0A, 0x0B},
Amounts: []CurrencyAmount{
{Amount: 1000000, Decimals: 2, Symbol: "RUB"},
{Amount: 2510, Decimals: 2, Symbol: "USD"},
},
IsBlocked: true,
}
// сериализуем значение переменной balance
msgpBz, err := balance.MarshalMsg(nil)
if err != nil {
panic(err)
}
var balanceCopy AccountBalance
// декодируем данные в переменную типа AccountBalance
if _, err := balanceCopy.UnmarshalMsg(msgpBz); err != nil {
panic(err)
}
// для сравнения выведем оригинальное и полученное значения
fmt.Printf("balanceInit: %+v\n", balance)
fmt.Printf("balanceCopy: %+v\n", balanceCopy)
}
balanceInit: {AccountIdHash:[16 32 10 11] Amounts:[{Amount:1000000 Decimals:2 Symbol:RUB} {Amount:2510 Decimals:2 Symbol:USD}] IsBlocked:true}
balanceCopy: {AccountIdHash:[16 32 10 11] Amounts:[{Amount:1000000 Decimals:2 Symbol:RUB} {Amount:2510 Decimals:2 Symbol:USD}] IsBlocked:true}
msgp
-реализации протокола MessagePack.protoc
(вот инструкция по установке) из proto-файлов можно сгенерировать специфичный для языка код работы с сериализуемыми типами.protoc
помогает генерировать связующий код для разных языков программирования (C++, JavaScript, Go, Rust и других). Чтобы protoc
генерировал код на языке Go, нужно ещё установить соответствующий плагин: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
.protoc
-утилиты важно знать структуру файлов проекта, а также его Go-путь. Допустим, есть Go-проект — protobuf
с такой структурой каталога:protobuf
│ main.go // сама программа
│
└───proto // все Protobuf-файлы
│ │ acc_balance.proto // proto-описание типов AccountBalance и CurrencyAmount
proto3
-спецификация (файл proto/acc_balance.proto
):syntax = "proto3";
// имя proto-пакета и версия
// версию указывать необязательно, это общепринятый подход
// для версионирования спецификации
package account.v1beta1;
// опция задаёт пакет для генерируемого файла
// файл будет создаваться в родительской директории с именем пакета main
option go_package = "./main";
// описание типа AccountBalance
message AccountBalance {
bytes account_id_hash = 1; // Go: []byte
bool is_blocked = 2; // Go: bool
repeated CurrencyAmount amounts = 3; // Go: []CurrencyAmount
}
// описание типа CurrencyAmount
message CurrencyAmount {
int64 amount = 1; // Go: int64
int32 decimals = 2; // Go: int32 (int8 не определён спецификацией proto3)
string symbol = 3; // Go: string
}
protobuf
и запустить protoc
с нужными параметрами:protoc --proto_path=proto --go_opt=paths=source_relative --go_out=. proto/acc_balance.proto
--proto_path
— путь до папки со всеми proto-файлами проекта.
Proto-файл может импортировать другие proto-файлы, поэтому указываем для protoc
, где искать локальные зависимости.--go_opt
— специфичные для Go опции.
Опция paths=source_relative
заставляет protoc
использовать относительные пути для генерируемых файлов.--go_out
— путь до папки, куда protoc
будет записывать генерируемые .go
-файлы.
Пишем рабочую папку проекта, так как при указании опции paths=source_relative
утилита сама создаст нужные папки.proto/acc_balance.proto
— путь до исходного proto-файла.
Можно передавать несколько файлов, разделённых пробелом../acc_balance.pb.go
с нужными Go-типами.protoc
не создаёт методы Marshal()
и Unmarshal()
для Go-типов. Нужно добавить в зависимости проекта библиотеку protobuf-go, которая определяет функции для работы с Protobuf. package main
import (
"fmt"
"github.com/golang/protobuf/proto"
)
func main() {
balance := AccountBalance{
AccountIdHash: []byte{0x10, 0x20, 0x0A, 0x0B},
Amounts: []*CurrencyAmount{
{Amount: 1000000, Decimals: 2, Symbol: "RUB"},
{Amount: 2510, Decimals: 2, Symbol: "USD"},
},
IsBlocked: true,
}
// сериализуем значение переменной balance
protoBz, err := proto.Marshal(&balance)
if err != nil {
panic(err)
}
var balanceCopy AccountBalance
// декодируем данные в новую переменную
if err := proto.Unmarshal(protoBz, &balanceCopy); err != nil {
panic(err)
}
// визуально сравниваем значения переменных
fmt.Printf("balanceInit: %s\n", balance.String())
fmt.Printf("balanceCopy: %s\n", balanceCopy.String())
}
balanceInit: account_id_hash:"\x10 \n\x0b" is_blocked:true amounts:{amount:1000000 decimals:2 symbol:"RUB"} amounts:{amount:2510 decimals:2 symbol:"USD"}
balanceCopy: account_id_hash:"\x10 \n\x0b" is_blocked:true amounts:{amount:1000000 decimals:2 symbol:"RUB"} amounts:{amount:2510 decimals:2 symbol:"USD"}
AccountBalance
AccountBalance
в сгенерированном файле выглядит так:type AccountBalance struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AccountIdHash []byte `protobuf:"bytes,1,opt,name=account_id_hash,json=accountIdHash,proto3" json:"account_id_hash,omitempty"` // Go: []byte
IsBlocked bool `protobuf:"varint,2,opt,name=is_blocked,json=isBlocked,proto3" json:"is_blocked,omitempty"` // Go: bool
Amounts []*CurrencyAmount `protobuf:"bytes,3,rep,name=amounts,proto3" json:"amounts,omitempty"` // Go: []CurrencyAmount
}
Amounts
тип []*CurrencyAmount
, то есть слайс указателей на CurrencyAmount
. Любые вложенные структуры определяются Protobuf как указатели, поэтому при инициализации экземпляра созданы указатели на CurrencyAmount
. В примере слайс указателей не согласуется с логикой приложения (эти объекты не должны быть nullable
), но это стандартное поведение protoc
изменить невозможно.protoc
создаёт для Go-типов метод String()
, поэтому в консоль объекты выводим через %s
, что делает консольный вывод более читаемым по сравнению с %+v
.yaml
, toml
.easyjson
— пакет для JSON-сериализации, требующий предварительной кодогенерации. Работает гораздо быстрее сериализатора стандартной библиотеки, потому что не использует рефлексию. Зато использует такие же структурные теги и совместим с пакетом encoding/json
.msg
и требует для структур кодогенерации необходимых методов.protoc
на основе описания структур данных и методов в proto
-файле.easyjson
), другие добавляют недостающую (Protobuf, MessagePack).easyjson
и другим).