📊 Эффективная работа с JSON в Go
JSON (JavaScript Object Notation) – это легкий формат обмена данными, основанный на синтаксисе объектов JavaScript. Он широко используется для передачи информации между клиентом и сервером в сетевых приложениях.
JSON представляет данные в виде пар «ключ-значение» и может содержать массивы, числа, строки, логические значения и null. Он обеспечивает простоту чтения и записи для людей, а также легкость разбора и генерации для компьютеров.
Чтение JSON
Десериализация (чтение JSON) – это процесс преобразования данных из формата JSON в объекты Go. Для этих целей используется пакет encoding/json
, который входит в стандартную библиотеку языка. Основная функция для чтения Unmarshal
считывает JSON-данные и сохраняет результат в значении, на которое указывает заданная переменная. Если же она представляет собой nil или не является указателем, то функция вернет InvalidUnmarshalError
.
Общая сигнатура: func Unmarshal(data []byte, v any) error
Рассмотрим применение функции Unmarshal
на конкретном примере:
type Worker struct {
Name string `json:"name"`
Age int `json:"age"`
Job string `json:"job"`
}
func main() {
var worker Worker
jsonData := `{"name":"Петя", "age":18, "job":"Backend-разработчик"}`
err := json.Unmarshal([]byte(jsonData), &worker)
if err != nil {
fmt.Println("Ошибка чтения JSON-данных:", err)
}
fmt.Println(worker)
}
В результате работы программы будет выведено "{Петя 18 программист}".
- Функция
Unmarshal
обладает интересной особенностью – она считывает только поля, соответствующие объявленным типам. Такое поведение используется для выборки определенных данных из большого JSON-файла.
В следующем примере будут заполнены только поля Name
и Age
, а Job
останется проигнорированным. Вывод будет следующий: "{Витя 20 }".
type Worker struct {
Name string `json:"name"`
Age int `json:"age"`
Job string `json:"job"`
}
...
var worker Worker
jsonData := `{"name":"Витя", "age": 20, "city":"Москва"}`
err := json.Unmarshal([]byte(jsonData), &worker)
Запись JSON
Сериализация (запись JSON) – это процесс преобразования объекта в формат, который можно сохранить или передать по сети. Для этого в Go используется функция Marshal()
из ранее рассмотренного пакета encoding/json
. Она возвращает два значения – срез байт и ошибку.
Её сигнатура имеет следующий вид: func Marshal(v any) ([]byte, error)
type Worker struct {
Name string `json:"name"`
Age int `json:"age"`
Job string `json:"job"`
}
func main() {
workerInfo := Worker{Name: "Ваня", Age: 14, Job: "Go-разработчик"}
jsonInfo, err := json.Marshal(workerInfo)
if err != nil {
fmt.Println("Ошибка записи данных:", err)
}
fmt.Println(jsonInfo)
}
Стоит учитывать, что записаны могут быть только структуры данных, представимые в виде корректного формата JSON.
- Для кодировки типа map он должен иметь вид
map[string]T
, где T – любой тип, поддерживаемый пакетомencoding/json
. - Циклические структуры данных могут привести к попаданию функции
Marshal
в бесконечный цикл, поэтому не поддерживаются для преобразования. - Для кодирования указателей необходимо представить их в виде значений, на которые они указывают. В случае nil-указателей это будет nil.
- Функции, каналы и тип complex не поддерживаются.
Потоковые кодировщики и декодеры
Интерфейсы Reader и Writer
В Go существуют два основных интерфейса из пакета io
стандартной библиотеки, io.Reader
и io.Writer
. Они широко используются для ввода/вывода данных, при работе с файлами, сетевыми соединениями и другими объектами, поддерживающими операции чтения и записи.
Интерфейс io.Reader
определяет метод Read
, который принимает в качестве параметра буфер для чтения, а возвращает количество байт, прочитанных из источника, и ошибку. При завершении потока данных io.Reader
возвращает ошибку io.EOF
(конец файла).
type Reader interface {
Read(p []byte) (n int, err error)
}
Интерфейс io.Writer
определяет метод Write
, который принимает в качестве параметра буфер для записи данных, а возвращает количество байт, записанных в целевой объект, и ошибку.
type Writer interface {
Write(p []byte) (n int, err error)
}
При желании каждый разработчик может написать собственные интерфейсы Reader
и Writer
, руководствуясь соглашениями языка.
Кодировщик json.Encoder
Структура json.Encoder
предназначена для кодирования JSON-данных и их последующей записи в выходной поток Writer
.
Так выглядит сигнатура функции для создания кодировщика: func NewEncoder(w io.Writer) *Encoder
type Shape struct {
Type string `json:"name"`
Width int `json:"width"`
Height int `json:"height"`
}
func main() {
// Создание слайса структур Workers с необходимыми данными
shapes := []Shape{
{Type: "Квадрат", Width: 10, Height: 10},
{Type: "Прямоугольник", Width: 50, Height: 20},
}
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
// Запись JSON-данных в буфер
if err := encoder.Encode(shapes); err != nil {
fmt.Println("Ошибка при записи JSON-данных:", err)
return
}
fmt.Println(buf.String())
}
Декодер json.Decoder
Структура json.Decoder
позволяет декодировать данные JSON из интерфейса Reader
. К примеру, из файла, буфера или сетевого соединения. Decoder
также обеспечивает возможность контроля синтаксических ошибок и обработки потока JSON-данных в режиме реального времени.
Сигнатура для создания декодера: func NewDecoder(r io.Reader) *Decoder
Рассмотрим пример использования этой функции.
type Student struct {
Name string `json:"name"`
Grade int `json:"grade"`
}
func main() {
jsonData := `{"name":"Иван", "grade":10}`
// Создание буфера с данными в формате JSON
reader := strings.NewReader(jsonData)
// Создание Decoder для чтения из буфера
decoder := json.NewDecoder(reader)
var student Student
// Чтение JSON из буфера и их запись в student
if err := decoder.Decode(&student); err != nil {
fmt.Println("Ошибка декодирования:", err)
return
}
fmt.Println(student.Name, student.Grade)
}
На экран будет выведено: Иван 10
Тип NullString
В Go есть специальный тип NullString
для работы с пустыми или отсутствующими значениями JSON-данных. Он позволяет явно указать, является ли строка пустой или нулевой.
Стандартная библиотека Go не предоставляет встроенного типа NullString
, но можно создать его самостоятельно, используя структуру или указатель на строку.
type NullString struct {
String string
Valid bool
}
Тип NullString
также бывает полезен для десериализации JSON-данных в нестандартном формате. С его помощью можно создавать собственные функции наподобие Unmarshal
.
В качестве примера создадим кастомный десериализатор с использованием NullString
.
type NullString struct {
String string
Valid bool
}
func (nstr *NullString) UnmarshalCustomData(jsonData []byte) error {
// Проверяем, являются ли JSON-данные нулевой строкой
if string(jsonData) == "null" {
nstr.Valid = false
return nil
}
var s string
// Десериализация JSON
if err := json.Unmarshal(jsonData, &s); err != nil {
return err
}
// Присвоение строки типу NullString
nstr.String = s
nstr.Valid = true
return nil
}
Декодирование неизвестных данных
Зачастую в JSON хранятся данные разных типов, которые необходимо правильно обработать. В этом случае может помочь декодирование значений в интерфейс и их последующий перебор с использованием switch-case. Такой подход позволяет сохранить преимущества безопасности типов.
Для детального понимания рассмотрим конкретный пример.
func main() {
jsonData := []byte(`{"name":"Ваня","grade":11,"classmates":["Петя", "Игорь","Глеб"]}`)
// Десериализация JSON-данных
var data interface{}
if err := json.Unmarshal(jsonData, &data); err != nil {
fmt.Println("Ошибка при чтении JSON:", err)
}
// Type assertion
res := data.(map[string]interface{})
// Перебор ключей и значений
for key, val := range res {
switch value := val.(type) {
case string:
fmt.Println(key, "имеет тип string - ", value)
case float64:
fmt.Println(key, "имеет тип float64 - ", value)
case []interface{}:
fmt.Println(key, "имеет тип []interface{} - ", value)
default:
fmt.Println("Значение элемента неизвестно")
}
}
}
Вывод будет следующий:
name имеет тип string - Ваня
grade имеет тип float64 - 11
classmates имеет тип []interface{} - [Петя Игорь Глеб]