Перевод одной из статей Бена Джонсона из серии "Go Walkthrough" по более углублённому изучению стандартной библиотеки Go в контексте реальных задач.


Пока что мы рассмотрели работу с потоками и слайсами байт, но мало какие программы просто гоняют байты туда сюда. Сами по себе байты много смысла не несут, а вот когда мы кодируем структуры данных с помощью этих байт, тогда мы можем создавать действительно полезные приложения.


Этот пост является одним из серии статей по более углублённому разбору стандартной библиотеки. Несмотря на то, что стандартная документация предоставляет массу полезной информации, в контексте реальных задач может быть непросто разобраться, что и когда использовать. Эта серия статей направлена на то, чтобы показать использование пакетов стандартной библиотеки в контексте реальных приложений. Если у вас есть вопросы или комментарии, вы всегда можете написать мне в Твиттер — @benbjohnson.


Что такое кодирование (encoding)?


В программировании нередко для простых концепций используются замудрёные слова. Даже больше того — часто для одной концепции существует несколько замудрёных слов. Кодирование (encoding) это одно из таких слов. Иногда оно называется сериализацией(serialization) или маршалинг (marshaling) — что означет одно и тоже: добавление логической структуры сырым байтам.


В стандартной библиотеке Go мы используем термины кодирование (encoding) и маршалинг (marshaling) для двух разных, но связанных идей. Encoder в Go это объект, который добавляет логическую структуру на поток байт, в то время как marshaling работает с ограниченным набором байт в памяти.


Например, в пакете encoding/json есть json.Encoder и json.Decoder для работы с io.Writer и io.Reader потоками соответственно. И также в этом пакете мы видим json.Marshaler и json.Unmarshaler для записи и чтения байт из слайса.


Два типа кодирования


Есть ещё одно важно различие в кодировании. Некоторые пакеты для кодирования оперируют с примитивами — строки, целые числа и т.д. Строки закодированы кодировками вроде ASCII или Unicode или любыми другими кодировками. Целые числа могут закодированы по разному, в зависимости от endianness или используя целочисленное кодирование с произвольной длиной. Даже сами байты часто могут закодированы используя схемы вроде Base64, чтобы превратить их в печатаемые символы.


Но всё же чаще, когда мы говорим про кодирование, мы думаем именно о кодировании объектов. Это означает превращение сложных структур в памяти таких как структуры, карты и слайсы в набор байт. В этом превращении приходится иметь дело с массой компромиссов и за много лет люди придумали множество различных способов кодирования.


Делая компромиссы


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


Есть много причин, почему формат байт в памяти не подходит для сохранения на диск или отправки в сеть. Во-первых, совместимость. Формат размещения байт в памяти Go объектов не совпадает с форматом объектов в Java, поэтому двум системам, написанным на разных языках, будет невозможно друг друга понимать. Также иногда нам нужна совместимость не только с другим языком программирования, но и с человеком. CSV, JSON и XML — это всё примеры человекочитаемых форматов, которые можно легко просмотреть и изменить вручную.


Впрочем, добавление человекочитаемости формату ставит нас перед компромиссом. Форматы, которые легко читаемы человеком, сложнее и дольше для чтения компьютером. Целые числа — хороший тому пример. Люди читают номера в десятичной форме, тогда как компьютер оперирует числами в двоичной форме. Люди также читают числа различной длины, вроде 1 или 1000, в то время, как компьютеры работают с числами фиксированного размера — 32 или 64 бит. Разница в производительности может показаться незначительной, но она быстро станет заметна при парсинге миллионов или миллирадов чисел.


Также есть один компромисс, о котором мы обычно не думаем поначалу. Наши структуры данных могут меняться со временем, но мы должны уметь работать с данными, закодированными много лет назад. Некоторые кодировки, вроде Protocol Buffers, позволяют описать схему для ваших данных и добавить версию к полям — старые поля могут быть объявлены устаревшими и добавлены новые. Минус тут в том, что нужно знать определение схемы вместе с данными, чтобы мочь закодировать или декодировать данные. Собственный формат Go — gob, использует другой подход и сохраняет схему данных прямо во время кодирования. Но тут минус в том, что размер закодированных данных становится довольно большим.


Некоторые форматы вообще обходят этот момент и идут без схемы. JSON и MessagePack позволяют кодировать структуры на лету, но не предоставляют никаких гарантий для безопасного декодирования со старых версий.


Мы также используем системы, которые делают кодирование за нас, но о которых мы не думаем, как о кодировщиках. Например, базы данных это тоже один из способов взять наши логические структуры и сохранить в виде набора байт на диске. Скорее всего там будет много всего — сетевые вызовы, парсинг SQL, планирование запросов, но, по сути, это кодирование байт.


В конце концов, если вам важнее всего скорость, вы можете использовать внутренний формат памяти Go и сохранять данные как есть. Я даже написал для этого библиотеку под названием raw. Время кодирования и декодирования тут буквально 0 секунд. Но лучше не стоит её использовать в продакшене.


4 интерфейса в encoding


Если вы являетесь одним из тех немногих людей, которые заглядывали в пакет encoding, вы можете быть слегка разочарованы. Это второй самый маленький пакет после errors и в нём находятся всего лишь 4 интерфейса.


Первые два — интерфейсы BinaryMarshaler и BinaryUnmarshaler.


type BinaryMarshaler interface {
        MarshalBinary() (data []byte, err error)
}
type BinaryUnmarshaler interface {
        UnmarshalBinary(data []byte) error
}

Они предназначены для объектов, которые предоставляют способ конвертировать в и из бинарного формата. Эти интерфейсы используются в нескольких местах в стандартной библиотеке, например в time.Time.MarshalBinary(). Вы не найдёте их много где, потому что обычно нет единого способа конвертировать данные в бинарную форму. Как мы уже увидели, есть огромное количество различных форматов сериализации.


Но на уровне приложения, вы скорее всего выберете какой-то один формат для кодирования. Например, вы можете выбрать Protocol Buffers для всех данных. Обычно нет смысла поддерживать сразу несколько бинарных форматов в приложении, поэтому реализация BinaryMarshaler может иметь смысл.


Следующие два интерфейса это TextMarshaler и TextUnmarshaler:


type TextMarshaler interface {
        MarshalText() (text []byte, err error)
}
type TextUnmarshaler interface {
        UnmarshalText(text []byte) error
}

Эти два интерфейса очень похожи на предыдущие, но они работают с данными в формате UTF-8.


Некоторые форматы определяют свои собственные интерфейсы для маршалинга, например json.Marshaler, и они следуют той же логике имён.


Обзор пакетов encoding


В стандартной библиотеке есть множество полезных пакетов для кодирования данных. Мы рассмотрим их более детально в следующих статьях, но тут я бы хотел сделать краткий обзор. Некоторые из пакетов лежат в encoding/, а некоторые находятся в других местах.


Кодировки базовых типов


Скорее всего, первый пакет, который вы использовали, когда только познакомились с Go был пакет fmt (произносится "fumpt"). Он используется printf() формат в стиле C для кодирования и декодирования чисел, строк, байт и даже имеет базовые возможности по кодированию объектов. Пакет fmt это отличный и простой способ создавать человеко-читаемые строки на основании шаблонов, но парсинг шаблонов может быть не сильно быстрым.


Если вам нужна более высокая производительность, вы можете уйти от printf-шаблонов и использовать пакет strconv. Это низкоуровневый пакет для базового форматирования и сканирования строк, целых и дробных чисел, логических значений, и в целом он достаточно быстрый.


Эти пакеты, как и сам Go, подразумевают, что вы работаете со строками в UTF-8. Почти полное отсутствие поддержки не-Unicode кодировок в стандартной библиотеке скорее всего объясняется тем, что интернет в последние годы очень быстро сошёлся в том, что всё должно быть в UTF-8, а возможно и потому, что Роб Пайк как раз придумал и Go, и UTF-8, кто знает. Мне, наверное, повезло и не пришлось сталкиваться с не-UTF-8 кодировками в Go, но, впрочем, есть такие пакеты как unicode/utf16, encoding/ascii85 и целая ветка golang.org/x/text. Эта ветка содержит большое количество отличных пакетов, которые являются частью проекта Go, но не попадают под гарантии обратной совместимости Go 1.


Для кодирования чисел, пакет encoding/binary предоставляет big endian и little endian кодирование, а также кодирование чисел переменной длины. Endianness — означает порядок, в котором байты идут один за другм. Например uint16 представление числа 1000 (0x03e8 в шестнадцатеричной форме) состоит из двух байт — 03 и e8. В big endian форме эти байты пишутся в таком порядке — "03 e8". В little endian, порядок обратный — "e8 03". Многие популярные архитектуры CPU являются little endian. Но big endian обычно используется для отправки данных по сети. Он даже так и называется — network byte order.


В заключение, есть пару пакетов для непосредственного кодирования самих байт. Обычно кодирование байт используется для перевода их в печатаемый формат. Например, пакет encoding/hex используется для представления данных в шестнадцатеричной форме. Я лично его использовал только для отладочных целей. С другой стороны, иногда вам нужны печатаемые символы, потому что вы хотите отправить данные по протоколам, в которых, по историческим причинам, ограниченная поддержка бинарных данных (email, например). Пакеты encoding/base32 и encoding/base64 являюется хорошими примерами. Ещё один пример — пакет encoding/pem, который используется для кодирования TLS сертификатов.


Кодирование объектов


Для кодирования объектов в стандартной библиотеке чуть меньше пакетов. Но, на практике, этих пакетов оказывается более, чем достаточно.


Если вы только не провели последние 10 лет в танке, то наверняка заметили, что JSON стал форматом для кодирования объектов по умолчанию. Как уже упоминалось ранее, у JSON есть свои недостатки, но его очень просто использовать и его реализация есть почти во всех языках, поэтому и огромная популярность. Пакет encoding/json предоставляет отличную поддержку этого формата, и, также, в Go есть сторонние, более быстрые, реализации парсеров, такие как ffjson.


И хотя JSON стал доминирующим протоколом обмена между машинами, формат CSV всё еще остается популярным для экспорта данных для людей. Пакет encoding/csv предоставляет хороший интерфейс для работы с табличными данными в этом формате.


Если вы работете с системами, написанными в районе 2000-х, наверняка вам понадобится работать с XML. Пакет encoding/xml предоставляет интерфейс в SAX-стиле для дополнительного основанного на тэгах кодирования/декодирования, похожий на аналогичный пакет для json. Если вам нужны более сложные манипуляции и штуки вроде DOM, XPath, XSD и XSLT, тогда вам, наверное нужно использовать libxml2 через cgo.


У Go также есть свой собственный формат для потокового кодирования — gob. Этот пакет используется в net/rpc для реализации удаленного вызова процедур между Go сервисами. Gob прост в использовании, но он не поддерживается в других языках. gRPC выглядит как более популярная альтернатива если вам нужен кросс-языковой инструмент.


И в заключение, есть пакет encoding/asn1. Документация по нему скромная и единственная ссылка ведёт на 25 страничную стену текста — введение для новичков в ASN.1. ASN.1 это сложная схема кодировки, которая используется, в основном X.509 сертификами в SSL/TLS.


Заключение


Кодирование даёт фундамент для придания логической структуры сырым байтам. Без него у нас бы не было ни строк, ни стркутур данных, ни баз данных или хоть сколько нибудь полезных приложений. То, что кажется простой концепцией, на самом деле имеет богатую историю реализаций и огромный набор компромиссов.


В этой статье мы рассмотрели различные реализации кодировок, реализованных в стандартной библиотеке и некоторых их компромиссы. Мы увидели, как эти пакеты для работы с базовыми типами и объектами построены на нашем понимании работы с байтами и потоками байт в Go. В следующих статьях мы заглянем глубже в эти пакеты и посмотрим, как их использовать в контексте реальных приложений.

Поделиться с друзьями
-->

Комментарии (1)


  1. AngelGabriel
    16.09.2016 10:10

    Про gob тема не раскрыта