JSON in GO
- пятница, 1 марта 2024 г. в 00:00:16
Это перевод одноименной статьи.
В стандартном пакете encoding/json
присутствуют механизмы сериализации marshaling
и десериализации unmarshaling
JSON.
Пример:
data, err := json.Marshal(yourVar)
Метод Marshal()
принимает переменную yourVar
любого типа, которую нужно сериализовать в JSON, и возвращает два значения: сериализованные данные в виде байтового массива ([]byte
) и ошибку (error
), если таковая возникает.
Пример:
data, err := json.Marshal(yourVar)
if err != nil {
return err
}
// можем использовать `data` без опасений
Ошибка возвращается только в случаях, когда невозможно корректно сериализовать данные. К примеру, если ваш объект содержит следующие типы, сериализация завершится с ошибкой:
Каналы
Комплексные числа
Функции
ch := make(chan struct{})
_, err := json.Marshal(ch) // returns error *json.UnsupportedTypeError
compl := complex(10, 11)
_, err = json.Marshal(compl) // returns error *json.UnsupportedTypeError
fn := func() {}
_, err = json.Marshal(fn) // returns error *json.UnsupportedTypeError
Какие поля структуры видны для json пакета?
Одна из распространенных ошибок — ожидание, что пакет json
сможет обработать как публичные (public
), так и приватные (private
) поля структур. На самом деле, пакет json
сериализует только публичные поля, имена которых начинаются с заглавной буквы. Приватные поля, начинающиеся с маленькой буквы, будут игнорироваться при сериализации, что важно учитывать при проектировании структур данных для работы с JSON.
Для десериализации JSON-данных в Go используется метод json.Unmarshal()
, позволяющий преобразовать данные из JSON обратно в структуру Go.
myVal := MyVal{}
byte := `{"some":"json"}`
err := json.Unmarhal(byte, &myVal)
При десериализации могут возникнуть следующие ошибки:
Если данные невозможно десериализовать из-за несоответствия типов или других проблем с форматом JSON.
Если в качестве второго аргумента передан не указатель, то есть изменения не могут быть применены к переданной переменной.
Если второй аргумент — nil
, что означает отсутствие целевого объекта для десериализации данных.
Обработка имен полей.
При десериализации json.Unmarshal
ищет совпадения имен полей в структуре с ключами в JSON. Если точное совпадение имени не найдено, поиск повторяется, игнорируя регистр букв. В случае, если в JSON отсутствует поле, соответствующее полю структуры, значение этого поля останется неизменным (то есть будет сохранено его начальное значение).
Мы можем использовать структурные теги для управления именами полей или изменения имен при декодировании.
Предположим у нас есть структура с двумя полями. Когда мы сериализуем эти поля, оба поля будут заглавными. Часто нам это и требуется.
type User struct {
ID string
Username string
}
// вывод будет примерно таким
{"ID":"some-id","Username":"admin"}
Для модификации этого поведения мы можем воспользоваться структурными тегами. После определения типа поля добавляется текст, где первым словом идет имя поля в формате JSON, далее следует разделитель :
, а после — значение тега в двойных кавычках, как показано в примере ниже.
type User struct {
ID string `json:"id"` // Сериализуется как "id"
Username string `json:"user"` // Сериализуется как "user"
}
u := User{ID: "some-id", Username: "admin"}
// вывод будет примерно таким
{"id":"some-id","user":"admin"}
В примере мы переименовали оба поля. Имя поля может быть любым валидным json ключом.
Опция omitempty
позволяет исключать поля из сериализованного JSON, если их значение равно нулю (или эквивалентно нулю для данного типа).
type User struct {
ID string `json:"id"`
Username string `json:"user"`
Age int `json:"age,omitempty"` // Пропускается, если значение 0
}
Если мы не хотим менять имя поля мы можем пропустить его, но не забывайте использовать запятую в случае использования omitempty
type User struct {
ID string `json:"id"`
Username string `json:"user"`
Age string `json:",omitempty"` // обратите внимание на запятую
}
Если мы хотим игнорировать публичное поле, мы должны использовать тэг -
type User struct {
ID string `json:"id"`
Username string `json:"user"`
Age int `json:"-"` // Полностью игнорируется при сериализации
}
u := User{ID: "some-id", Username: "admin", Age: 18}
// поле age отсутствует:
{"id":"some-id","user":"admin"}
Важно помнить:
Структурные теги обрабатываются во время выполнения программы (runtime).
Компилятор не выдаст ошибки за неправильно сформированные структурные теги, что означает необходимость внимательной проверки синтаксиса тегов.
Опция omitempty
и тег -
предоставляют гибкость в управлении выводом JSON, позволяя создавать более чистый и оптимизированный вывод.
В пакете encoding/json
Go также представлены json.Decoder
и json.Encoder
, которые дополняют функциональность json.Marshal()
и json.Unmarshal()
.
Основное различие между этими инструментами заключается в том, что json.Encoder
и json.Decoder
предназначены для работы с потоками данных и напрямую взаимодействуют с объектами, поддерживающими интерфейс io.Writer
для записи и io.Reader
для чтения, соответственно. В отличие от этого, json.Marshal()
и json.Unmarshal()
оперируют массивами байтов.
Это делает json.Decoder
и json.Encoder
предпочтительными для использования в ситуациях, когда требуется обрабатывать данные на лету, еще до их полного получения или когда работа ведется с потоковыми данными.
Давайте, для примера, реализуем чтение тела запроса. Будем использовать и Unmarshal и Decoder.
req := CreateOrderRequest{}
if err := json.Decoder(r.Body).Decode(&req); err != nil {
// обработка ошибки
}
// req готов к использованию
Теперь используем Unmarshal для сравнения читабельности кода.
req := CreateOrderRequest{}
body, err := io.ReadAll(r.Body)
if err != nil {
// обработка ошибки
}
if err = json.Unmarshal(body, &req); err != nil {
// обработка ошибки
}
Еще одно различие, которое может подсказать нам, какой инструмент выбрать, заключается в том, что мы можем многократно применять json.Decoder
и json.Encoder
к одному и тому же io.Reader
и io.Writer
. Это означает, что если поток данных, передаваемый декодеру, содержит несколько объектов JSON, мы можем создать декодер один раз, но вызывать метод Decode()
многократно.
req := CreateOrderRequest{}
decoder := json.Decoder(r.Body)
for err := decoder.Decode(&req); err != nil {
// обработка одного запроса
}
В случае, когда у нас есть данные в формате []byte
и мы предпочитаем использовать json.Decoder
для их обработки, нам может помочь использование пакета bytes
и его компонента Buffer
. Buffer
позволяет легко преобразовать наш слайс байтов в поток (io.Reader
), который необходим для работы с json.Decoder
.
var body []byte
buf := bytes.NewBuffer(body)
decoder := json.Decoder(buf)
for err := decoder.Decode(&req); err != nil {
// обработка одного запроса
}
Я написал простые тесты, чтобы сравнить оба подхода.
package jsons
import (
"bytes"
"encoding/json"
"io"
"testing"
)
var j = []byte(`{"user":"Johny Bravo","items":[{"id":"4983264583302173928","qty": 5}]}`)
var createRequest = CreateOrderRequest{
User: "Johny Bravo",
Items: []OrderItem{
{ID: "4983264583302173928", Qty: 5},
},
}
var err error
var body []byte
// OrderItem представляет элемент заказа.
type OrderItem struct {
ID string `json:"id"` // Идентификатор элемента
Qty int `json:"qty"` // Количество
}
// CreateOrderRequest описывает запрос на создание заказа.
type CreateOrderRequest struct {
User string `json:"user"` // Пользователь, совершающий заказ
Items []OrderItem `json:"items"` // Список элементов заказа
}
// BenchmarkJsonUnmarshal измеряет производительность функции json.Unmarshal.
func BenchmarkJsonUnmarshal(b *testing.B) {
b.ReportAllocs() // Отчет о выделениях памяти
req := CreateOrderRequest{}
b.ResetTimer() // Сброс таймера для чистого измерения
for i := 0; i < b.N; i++ {
err = json.Unmarshal(j, &req) // Десериализация JSON в структуру
}
}
// BenchmarkJsonDecoder измеряет производительность использования json.Decoder.
func BenchmarkJsonDecoder(b *testing.B) {
b.ReportAllocs()
req := CreateOrderRequest{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer() // Остановка таймера на время подготовки
buff := bytes.NewBuffer(j) // Создание буфера для чтения
b.StartTimer() // Возобновление измерения времени
decoder := json.NewDecoder(buff) // Создание декодера
err = decoder.Decode(&req) // Декодирование JSON в структуру
}
}
// BenchmarkJsonMarshal измеряет производительность функции json.Marshal.
func BenchmarkJsonMarshal(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
body, err = json.Marshal(createRequest) // Сериализация структуры в JSON
}
}
// BenchmarkJsonEncoder измеряет производительность использования json.Encoder.
func BenchmarkJsonEncoder(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
encoder := json.NewEncoder(io.Discard) // Создание энкодера, вывод в /dev/null
err = encoder.Encode(createRequest) // Кодирование структуры в JSON
}
}
После запуска (по крайней мере на моих данных) json.Unmarshal()
в три раза быстрее чем json.Decoder
.
С другой стороны json.Marshal()
и json.Encoder
показывают схожие результаты.
BenchmarkJsonUnmarshal-10 1345796 894.4 ns/op 336 B/op 9 allocs/op
BenchmarkJsonDecoder-10 522276 2226 ns/op 1080 B/op 13 allocs/op
BenchmarkJsonMarshal-10 6257662 193.1 ns/op 128 B/op 2 allocs/op
BenchmarkJsonEncoder-10 6867033 174.9 ns/op 48 B/op 1 allocs/op
Я призываю вас не полагаться на мои результаты, а провести подобные тесты на своих приложениях.
Вы, возможно, заметили, что JSON-файл, полученный с помощью функций json.Marshal
или json.Encoder
, представляет собой компактную строку. Это означает, что он не содержит дополнительных пробелов, которые могли бы сделать его более читаемым для человека. В результате такой JSON идеален для передачи данных, но может быть неудобен для восприятия.
В качестве решения этой проблемы можно использовать функцию json.MarshalIndent
, которая позволяет отформатировать вывод JSON, делая его более удобным для чтения.
Пример использования json.MarshalIndent
:
data := map[string]int{
"a": 1,
"b": 2,
}
b, err := json.MarshalIndent(data, "<префикс>", "<отступ>")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
// вывод
{
<префикс><отступ>"a": 1,
<префикс><отступ>"b": 2
<префикс>}
Чтобы задать для типа собственные правила сериализации, нужно добавить методы MarshalJSON()
и UnmarshalJSON()
:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Рассмотрим на примере. Предположим, мы получаем JSON с характеристиками компьютера, где размеры оперативной памяти и диска указаны в байтах, и нам нужно представить эти данные в более читаемом виде.
{
"cpu": "Intel Core i5",
"operatingSystem": "Windows 11",
"memory": 17179869184,
"storage": 274877906944
}
В таком формате данные сложно воспринимать. Подготовим структуру для их хранения:
type PC struct {
CPU string
OperatingSystem string
Memory string
Storage string
}
Для решения задачи потребуется создать новый тип. Мы реализуем для него метод UnmarshalJSON
, который позволит кастомизировать процесс десериализации JSON в этот тип.
type Memory string // Определяем тип Memory как строку
// UnmarshalJSON - специализированный метод для десериализации данных JSON в тип Memory.
// Этот метод реализует интерфейс json.Unmarshaler, позволяя кастомизировать процесс десериализации.
func (m *Memory) UnmarshalJSON(b []byte) error {
// Преобразуем входящие байты в строку и пытаемся конвертировать в число.
// Это число предполагается быть размером памяти в байтах.
size, err := strconv.Atoi(string(b))
if err != nil {
return err // Возвращаем ошибку, если конвертация не удалась
}
// Перебираем предопределенные размеры памяти и соответствующие суффиксы (например, MB, GB).
for i, d := range memorySizes {
if size > d {
// Если размер больше текущего делителя, форматируем вывод используя этот делитель
// и соответствующий суффикс, чтобы получить читаемое представление объема памяти.
*m = Memory(fmt.Sprintf("%d %s", size/d, sizeSuffixes[i]))
return nil // Завершаем функцию успешно
}
}
// Если размер меньше любого из предопределенных делителей, выводим его как количество байт.
*m = Memory(fmt.Sprintf("%d b", size))
return nil
}
Этот метод позволяет десериализовать численное значение размера памяти из JSON в удобочитаемый формат с использованием соответствующих единиц измерения (например, GB, MB).
Полный код
Если вы не уверены в структуре входящего JSON, у вас есть несколько способов обработать его. Одним из вариантов является использование словарей (maps). Например, если вы получаете JSON и хотите работать с ним динамически:
req := map[string]interface{}{}
if err != json.Decoder(r.Body).Decode(&req); err != nil {
// обработка err
}
Таким образом, вы помещаете все данные в словарь. Теперь можно итерировать по нему и разрабатывать логику в зависимости от содержимого. Для определения типов данных можно использовать рефлексию:
for k, v := range req {
refVal := reflect.TypeOf(v)
fmt.Printf("ключ '%s' содержит значение типа %s\n", k, refVal)
}
/* пример вывода:
ключ 'two' содержит значение типа string
ключ 'three' содержит значение типа float64
ключ 'one' содержит значение типа int
*/
Если после сериализации ваши поля не отображаются в JSON, это может быть связано с одной из двух причин:
Поле не является публичным. В Go публичные поля начинаются с заглавной буквы. Если ваше поле начинается со строчной буквы, оно считается приватным и не будет участвовать в сериализации.
Использован тэг json:"-"
для поля. Этот тэг указывает библиотеке сериализации пропустить данное поле и не включать его в результирующий JSON. Пример использования.
Строго говоря, нет. Но если вам кажется, что это необходимо, убедитесь, что ваши юнит-тесты надежно покрывают все случаи сериализации, которые могут привести к ошибке. В таком контексте, возможно, можно обойтись без явной проверки ошибки. Однако, не забывайте ясно комментировать в коде, почему проверка ошибки опущена, и каким образом тесты обеспечивают надежность вашего подхода. Это важно для понимания и безопасности вашего кода в долгосрочной перспективе.
Для большинства задач, связанных с JSON в Go, стандартного пакета encoding/json
вполне достаточно. Он обеспечивает все необходимые инструменты для сериализации и десериализации JSON, поддерживая при этом чистоту и простоту кода. Однако, если вы сталкиваетесь с особо крупными JSON-файлами или обрабатываете их в большом количестве, может появиться необходимость в поиске альтернативных решений, способных обеспечить более высокую производительность. В таких случаях стоит рассмотреть использование специализированных библиотек. В остальном же, стандартный пакет отлично справляется с задачами по работе с JSON.
Если вы ищете более быструю альтернативу, вы можете обратить внимание на https://github.com/goccy/go-json. Это замена для стандартного пакета encoding/json.
Если JSON файл огромен, но вам нужна только его часть, вы можете использовать https://github.com/buger/jsonparser, который позволяет анализировать только часть всего файла.