📊 Эффективная работа с JSON в Go

В статье рассматриваются основные подходы для работы с JSON в языке Go. Большое внимание уделено определениям из документации, а также конкретным примерам.

Введение в JSON

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)
👨‍💻 Библиотека Go разработчика
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»
🧩 Библиотека задач по Go
Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»
🎓 Библиотека Go для собеса
Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»

Запись 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{} -  [Петя Игорь Глеб]

Материалы по теме

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

Библиотека программиста
23 ноября 2018

Go vs Python: изучение основ языка Go в сравнении с Python

Это не соревнование двух языков, а просто еще один способ обучения. Рассмат...
admin
19 сентября 2018

TOП-3 языка программирования, которые нужно выучить до 2019

Это не просто три лучших языка программирования, а в некотором смысле попыт...