Розбираємося в Go: пакет encoding

Переклад однієї з статей Бена Джонсона з серії "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. У наступних статтях ми заглянемо глибше в ці пакети і подивимося, як їх використовувати в контексті реальних додатків.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.