$ sudo teach IT
МОДУЛЬ 5 · УРОК 5.2

Работа с JSON

Учимся превращать данные Go в JSON и обратно, читать JSON из файлов и отправлять по сети — фундамент любого реального приложения

Практически каждое современное приложение работает с JSON. Когда вы открываете прогноз погоды в телефоне, когда бот в Telegram отвечает на ваше сообщение, когда сайт загружает список товаров — данные передаются именно в формате JSON. В Go есть мощный пакет encoding/json, который позволяет легко превращать структуры Go в JSON-строку (маршалинг) и разбирать JSON обратно в структуры (анмаршалинг). В этом уроке мы разберём обе операции, научимся читать JSON из файлов, а в конце напишем полноценную мини-программу для сохранения и загрузки данных. Это критически важный навык для нашего финального проекта — CLI-утилиты.

📄 Что такое JSON и зачем он нужен

JSON (JavaScript Object Notation) — это текстовый формат для хранения и передачи данных. Он выглядит как набор пар «ключ-значение» и читается одинаково хорошо и человеком, и компьютером. Вот простой пример JSON:

{
    "name": "Алексей",
    "age": 25,
    "is_student": true,
    "hobbies": ["Go", "музыка", "шахматы"]
}

Обратите внимание: ключи всегда в двойных кавычках, строки — тоже в двойных кавычках, числа и true/false — без кавычек. Массивы — в квадратных скобках. Вложенные объекты — в фигурных скобках.

Где используется JSON: API веб-сервисов (Twitter, YouTube, погодные сервисы), конфигурационные файлы, обмен данными между микросервисами, хранение настроек приложений, базы данных (MongoDB, CouchDB).

В Go для работы с JSON используется стандартный пакет encoding/json. Никаких сторонних библиотек устанавливать не нужно — всё идёт «из коробки». Две главные операции:

Marshal (маршалинг) — превращение данных Go (структур, map, слайсов) в JSON-строку.

Unmarshal (анмаршалинг) — разбор JSON-строки обратно в данные Go.

🔧 Marshal — превращаем данные Go в JSON

Представьте, что у вас есть структура с информацией о пользователе. Вы хотите отправить эти данные на сервер или сохранить в файл. Для этого нужно превратить структуру в JSON-строку. Именно это делает функция json.Marshal.

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name    string
    Age     int
    IsAdmin bool
}

func main() {
    user := User{
        Name:    "Алексей",
        Age:     25,
        IsAdmin: false,
    }

    // Превращаем структуру в JSON ([]byte)
    data, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    // Преобразуем []byte в строку для вывода
    fmt.Println(string(data))
}

// Вывод: {"Name":"Алексей","Age":25,"IsAdmin":false}

Функция json.Marshal возвращает два значения: срез байтов ([]byte) с JSON-данными и ошибку. Если ошибки нет, err равен nil. Обратите внимание, что ключи в JSON совпадают с именами полей структуры: Name, Age, IsAdmin.

Теги структур — управляем именами полей

В реальных API ключи JSON обычно пишутся в нижнем регистре: name, age, is_admin. А поля структур Go должны начинаться с заглавной буквы (иначе они не экспортируются и json.Marshal их не увидит). Как быть? Go решает эту проблему с помощью тегов структур (struct tags):

type User struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    IsAdmin bool   `json:"is_admin"`
}

func main() {
    user := User{
        Name:    "Алексей",
        Age:     25,
        IsAdmin: false,
    }

    data, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    fmt.Println(string(data))
}

// Вывод: {"name":"Алексей","age":25,"is_admin":false}

Тег пишется после типа поля в обратных кавычках: `json:"имя_в_json"`. Теперь в JSON будут именно те ключи, которые мы указали.

💡 Важно: Поля структуры с маленькой буквы (name, age) являются неэкспортируемыми. Пакет encoding/json работает через рефлексию и видит только экспортируемые поля (с большой буквы). Поэтому всегда пишите поля с заглавной буквы и используйте теги для настройки имён.

Полезные опции тегов

Теги поддерживают дополнительные опции через запятую:

type Product struct {
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Secret   string  `json:"-"`              // Поле полностью игнорируется
    Discount float64 `json:"discount,omitempty"` // Пропускается, если значение нулевое
}

func main() {
    p := Product{
        ID:       1,
        Name:     "Ноутбук",
        Price:    59990.0,
        Secret:   "секретные данные",
        Discount: 0, // нулевое значение
    }

    data, _ := json.Marshal(p)
    fmt.Println(string(data))
}

// Вывод: {"id":1,"name":"Ноутбук","price":59990}
// Secret — отсутствует (тег "-")
// Discount — отсутствует (omitempty + нулевое значение)

json:"-" — поле полностью исключается из JSON. Удобно для паролей, токенов и внутренних данных.

omitempty — если значение равно нулю (0, "", false, nil), поле не включается в JSON. Удобно для необязательных полей.

MarshalIndent — красивый JSON

Функция json.Marshal выдаёт JSON в одну строку. Для отладки и записи в файлы удобнее форматированный JSON с отступами. Для этого есть json.MarshalIndent:

data, err := json.MarshalIndent(user, "", "    ")
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}
fmt.Println(string(data))

// Вывод:
// {
//     "name": "Алексей",
//     "age": 25,
//     "is_admin": false
// }

Второй аргумент — префикс (обычно пустая строка), третий — символ отступа (обычно 4 пробела или табуляция "\t").

📥 Unmarshal — разбираем JSON в структуры Go

Обратная операция — когда у вас есть JSON-строка (пришла из файла, из сети, из базы данных) и нужно превратить её в структуру Go. Для этого используется json.Unmarshal:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    IsAdmin bool   `json:"is_admin"`
}

func main() {
    // JSON-строка (обычно приходит из файла или сети)
    jsonStr := `{"name": "Мария", "age": 30, "is_admin": true}`

    // Создаём переменную, куда запишутся данные
    var user User

    // Разбираем JSON в структуру
    err := json.Unmarshal([]byte(jsonStr), &user)
    if err != nil {
        fmt.Println("Ошибка разбора JSON:", err)
        return
    }

    fmt.Println("Имя:", user.Name)       // Имя: Мария
    fmt.Println("Возраст:", user.Age)     // Возраст: 30
    fmt.Println("Админ:", user.IsAdmin)   // Админ: true
}

Обратите внимание на несколько ключевых моментов:

1. json.Unmarshal принимает []byte, поэтому строку нужно преобразовать: []byte(jsonStr).

2. Второй аргумент — указатель на переменную (&user). Функция должна записать данные внутрь этой переменной, поэтому нужен именно указатель.

3. Если JSON содержит поля, которых нет в структуре, они просто игнорируются. Если в структуре есть поля, которых нет в JSON, они останутся со значениями по умолчанию.

💡 Мягкий разбор: Go не ругается, если в JSON есть лишние поля или если каких-то полей не хватает. Это очень удобно: вы описываете в структуре только те поля, которые вам нужны, а остальные просто пропускаются.

Разбор массива JSON

JSON часто содержит массив объектов. В Go ему соответствует слайс структур:

package main

import (
    "encoding/json"
    "fmt"
)

type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

func main() {
    jsonData := `[
        {"id": 1, "title": "Купить молоко", "done": false},
        {"id": 2, "title": "Написать код", "done": true},
        {"id": 3, "title": "Позвонить другу", "done": false}
    ]`

    var tasks []Task

    err := json.Unmarshal([]byte(jsonData), &tasks)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    for _, t := range tasks {
        status := "❌"
        if t.Done {
            status = "✅"
        }
        fmt.Printf("%s %s (ID: %d)\n", status, t.Title, t.ID)
    }
}

// Вывод:
// ❌ Купить молоко (ID: 1)
// ✅ Написать код (ID: 2)
// ❌ Позвонить другу (ID: 3)

Разбор в map (когда структура неизвестна)

Иногда вы не знаете заранее, какие поля придут в JSON. Тогда можно разобрать данные в map[string]interface{} (или map[string]any в Go 1.18+):

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonStr := `{"name": "Алексей", "age": 25, "scores": [90, 85, 95]}`

    var result map[string]any

    err := json.Unmarshal([]byte(jsonStr), &result)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    fmt.Println("Имя:", result["name"])     // Имя: Алексей
    fmt.Println("Возраст:", result["age"])   // Возраст: 25
    fmt.Println("Оценки:", result["scores"]) // Оценки: [90 85 95]

    // Внимание: числа из JSON всегда приходят как float64!
    age := result["age"].(float64)
    fmt.Printf("Возраст (int): %d\n", int(age)) // Возраст (int): 25
}

💡 Ловушка с числами: При разборе в map[string]any все числа из JSON приходят как float64, даже если в JSON написано 25 без точки. Если нужен int, преобразуйте явно: int(value.(float64)). Это одна из причин, почему лучше использовать структуры — там типы определяются полем.

🏗️ Вложенные структуры и сложный JSON

Реальный JSON часто содержит вложенные объекты. В Go им соответствуют вложенные структуры:

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    City   string `json:"city"`
    Street string `json:"street"`
}

type Person struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Address Address `json:"address"`
}

func main() {
    jsonStr := `{
        "name": "Иван",
        "age": 28,
        "address": {
            "city": "Москва",
            "street": "ул. Пушкина, д. 10"
        }
    }`

    var person Person
    err := json.Unmarshal([]byte(jsonStr), &person)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    fmt.Println("Имя:", person.Name)
    fmt.Println("Город:", person.Address.City)
    fmt.Println("Улица:", person.Address.Street)
}

// Вывод:
// Имя: Иван
// Город: Москва
// Улица: ул. Пушкина, д. 10

Вложенность может быть любой глубины. Главное — структура Go должна повторять структуру JSON. Поле Address в Person имеет тип Address — это отдельная структура со своими полями и тегами.

📂 Чтение JSON из файла

В реальных приложениях JSON почти всегда хранится в файлах, а не в строковых литералах. Есть два способа прочитать JSON из файла: загрузить весь файл в память и использовать json.Unmarshal, или использовать потоковый декодер json.NewDecoder.

Способ 1: os.ReadFile + json.Unmarshal

Самый простой и понятный способ. Сначала читаем весь файл в память, потом разбираем:

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Config struct {
    Host string `json:"host"`
    Port int    `json:"port"`
    Debug bool  `json:"debug"`
}

func main() {
    // Читаем весь файл в []byte
    data, err := os.ReadFile("config.json")
    if err != nil {
        fmt.Println("Не удалось прочитать файл:", err)
        return
    }

    // Разбираем JSON
    var config Config
    err = json.Unmarshal(data, &config)
    if err != nil {
        fmt.Println("Ошибка разбора JSON:", err)
        return
    }

    fmt.Printf("Сервер: %s:%d (debug: %v)\n", config.Host, config.Port, config.Debug)
}

Допустим, файл config.json содержит:

{
    "host": "localhost",
    "port": 8080,
    "debug": true
}

Тогда программа выведет: Сервер: localhost:8080 (debug: true).

Способ 2: json.NewDecoder (потоковый)

Декодер работает с io.Reader — читает данные потоком, не загружая весь файл в память. Это эффективнее для больших файлов и необходимо при работе с HTTP-ответами:

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Config struct {
    Host  string `json:"host"`
    Port  int    `json:"port"`
    Debug bool   `json:"debug"`
}

func main() {
    // Открываем файл
    file, err := os.Open("config.json")
    if err != nil {
        fmt.Println("Не удалось открыть файл:", err)
        return
    }
    defer file.Close()

    // Создаём декодер и читаем
    var config Config
    decoder := json.NewDecoder(file)
    err = decoder.Decode(&config)
    if err != nil {
        fmt.Println("Ошибка декодирования:", err)
        return
    }

    fmt.Printf("Сервер: %s:%d (debug: %v)\n", config.Host, config.Port, config.Debug)
}

os.ReadFile + Unmarshal — простой и понятный, хорош для небольших файлов.

json.NewDecoder — работает с потоками (io.Reader), не грузит весь файл в память. Используйте для HTTP-ответов и больших файлов.

💾 Запись JSON в файл

Обратная задача — сохранить данные из программы в JSON-файл. Тоже два способа:

Способ 1: json.MarshalIndent + os.WriteFile

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

func main() {
    tasks := []Task{
        {ID: 1, Title: "Изучить JSON в Go", Done: true},
        {ID: 2, Title: "Написать CLI-утилиту", Done: false},
        {ID: 3, Title: "Сдать проект", Done: false},
    }

    // Превращаем в красивый JSON
    data, err := json.MarshalIndent(tasks, "", "    ")
    if err != nil {
        fmt.Println("Ошибка маршалинга:", err)
        return
    }

    // Записываем в файл (0644 — права на чтение/запись)
    err = os.WriteFile("tasks.json", data, 0644)
    if err != nil {
        fmt.Println("Ошибка записи:", err)
        return
    }

    fmt.Println("Задачи сохранены в tasks.json!")
}

После запуска появится файл tasks.json:

[
    {
        "id": 1,
        "title": "Изучить JSON в Go",
        "done": true
    },
    {
        "id": 2,
        "title": "Написать CLI-утилиту",
        "done": false
    },
    {
        "id": 3,
        "title": "Сдать проект",
        "done": false
    }
]

Способ 2: json.NewEncoder (потоковый)

Аналогично декодеру, есть энкодер, который пишет JSON прямо в io.Writer:

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

func main() {
    tasks := []Task{
        {ID: 1, Title: "Изучить JSON в Go", Done: true},
        {ID: 2, Title: "Написать CLI-утилиту", Done: false},
    }

    file, err := os.Create("tasks.json")
    if err != nil {
        fmt.Println("Ошибка создания файла:", err)
        return
    }
    defer file.Close()

    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "    ") // Красивое форматирование
    err = encoder.Encode(tasks)
    if err != nil {
        fmt.Println("Ошибка кодирования:", err)
        return
    }

    fmt.Println("Задачи сохранены!")
}

💡 Подсказка: json.NewEncoder удобен тем, что вам не нужно хранить весь JSON в памяти — данные пишутся напрямую в файл (или HTTP-ответ, или любой другой io.Writer). Метод SetIndent добавляет отступы.

🌐 Декодирование JSON из сети

Одна из самых частых задач — получить JSON по HTTP и разобрать его. В Go это делается элегантно, потому что http.Response.Body реализует io.Reader, а json.NewDecoder принимает именно io.Reader:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// Структура для ответа API
type Post struct {
    UserID int    `json:"userId"`
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Body   string `json:"body"`
}

func main() {
    // Делаем GET-запрос к тестовому API
    resp, err := http.Get("https://jsonplaceholder.typicode.com/posts/1")
    if err != nil {
        fmt.Println("Ошибка запроса:", err)
        return
    }
    defer resp.Body.Close()

    // Декодируем JSON прямо из тела ответа
    var post Post
    err = json.NewDecoder(resp.Body).Decode(&post)
    if err != nil {
        fmt.Println("Ошибка декодирования:", err)
        return
    }

    fmt.Printf("Пост #%d\n", post.ID)
    fmt.Printf("Автор (UserID): %d\n", post.UserID)
    fmt.Printf("Заголовок: %s\n", post.Title)
    fmt.Printf("Текст: %s\n", post.Body)
}

Разберём по шагам:

1. http.Get отправляет GET-запрос и возвращает ответ. resp.Body — это io.ReadCloser (умеет читать и закрываться).

2. defer resp.Body.Close() — обязательно закрываем тело ответа, чтобы не было утечки ресурсов.

3. json.NewDecoder(resp.Body).Decode(&post) — создаём декодер из тела ответа и сразу разбираем JSON в структуру.

Для получения массива объектов из API логика такая же, только переменная будет слайсом:

// Получаем список постов
resp, err := http.Get("https://jsonplaceholder.typicode.com/posts")
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}
defer resp.Body.Close()

var posts []Post
err = json.NewDecoder(resp.Body).Decode(&posts)
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

fmt.Printf("Получено постов: %d\n", len(posts))
for _, p := range posts[:5] { // Выводим первые 5
    fmt.Printf("  #%d: %s\n", p.ID, p.Title)
}

💡 Запомните паттерн: Для HTTP-ответов всегда используйте json.NewDecoder(resp.Body), а не io.ReadAll + json.Unmarshal. Декодер эффективнее по памяти и элегантнее по коду. Подробнее о HTTP-запросах — в следующем уроке 5.3.

⚠️ Обработка ошибок при работе с JSON

JSON может быть невалидным, содержать неожиданные типы или не соответствовать ожидаемой структуре. Всегда проверяйте ошибки:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    // Невалидный JSON — отсутствует закрывающая скобка
    badJSON := `{"name": "Алексей", "age": 25`

    var user User
    err := json.Unmarshal([]byte(badJSON), &user)
    if err != nil {
        fmt.Println("Ошибка:", err)
        // Вывод: Ошибка: unexpected end of JSON input
        return
    }

    // Неправильный тип данных
    wrongType := `{"name": "Алексей", "age": "двадцать пять"}`

    err = json.Unmarshal([]byte(wrongType), &user)
    if err != nil {
        fmt.Println("Ошибка типа:", err)
        // Вывод: Ошибка типа: json: cannot unmarshal string
        //        into Go struct field User.age of type int
        return
    }
}

Go сообщает точно, что пошло не так: невалидный JSON, неправильный тип поля, неожиданный конец данных. Это помогает быстро найти и исправить проблему.

Типичные ошибки при работе с JSON:

• Невалидный JSON (пропущена скобка, запятая, кавычка)

• Несовпадение типов (строка вместо числа и наоборот)

• Передача значения вместо указателя в Unmarshal

• Неэкспортируемые поля структуры (с маленькой буквы)

🚀 Практика: менеджер задач с сохранением в JSON

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

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

// Структура задачи
type Task struct {
    ID    int    `json:"id"`
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

const tasksFile = "tasks.json"

// loadTasks загружает задачи из файла
func loadTasks() ([]Task, error) {
    // Проверяем, существует ли файл
    data, err := os.ReadFile(tasksFile)
    if err != nil {
        // Если файла нет — возвращаем пустой список
        if os.IsNotExist(err) {
            return []Task{}, nil
        }
        return nil, fmt.Errorf("ошибка чтения файла: %w", err)
    }

    var tasks []Task
    err = json.Unmarshal(data, &tasks)
    if err != nil {
        return nil, fmt.Errorf("ошибка разбора JSON: %w", err)
    }

    return tasks, nil
}

// saveTasks сохраняет задачи в файл
func saveTasks(tasks []Task) error {
    data, err := json.MarshalIndent(tasks, "", "    ")
    if err != nil {
        return fmt.Errorf("ошибка маршалинга: %w", err)
    }

    err = os.WriteFile(tasksFile, data, 0644)
    if err != nil {
        return fmt.Errorf("ошибка записи файла: %w", err)
    }

    return nil
}

// addTask добавляет новую задачу
func addTask(tasks []Task, title string) []Task {
    // Определяем ID: максимальный + 1
    maxID := 0
    for _, t := range tasks {
        if t.ID > maxID {
            maxID = t.ID
        }
    }

    task := Task{
        ID:    maxID + 1,
        Title: title,
        Done:  false,
    }

    return append(tasks, task)
}

// completeTask отмечает задачу выполненной
func completeTask(tasks []Task, id int) bool {
    for i := range tasks {
        if tasks[i].ID == id {
            tasks[i].Done = true
            return true
        }
    }
    return false
}

// printTasks выводит все задачи
func printTasks(tasks []Task) {
    if len(tasks) == 0 {
        fmt.Println("Список задач пуст.")
        return
    }

    fmt.Println("=== Список задач ===")
    for _, t := range tasks {
        status := "[ ]"
        if t.Done {
            status = "[x]"
        }
        fmt.Printf("  %s #%d: %s\n", status, t.ID, t.Title)
    }
    fmt.Println()
}

func main() {
    // 1. Загружаем задачи из файла
    tasks, err := loadTasks()
    if err != nil {
        fmt.Println("Ошибка загрузки:", err)
        return
    }

    fmt.Println("Задачи загружены из файла.")
    printTasks(tasks)

    // 2. Добавляем новые задачи
    tasks = addTask(tasks, "Изучить encoding/json")
    tasks = addTask(tasks, "Прочитать про HTTP в Go")
    tasks = addTask(tasks, "Написать финальный проект")

    // 3. Отмечаем первую задачу выполненной
    if completeTask(tasks, 1) {
        fmt.Println("Задача #1 выполнена!")
    }

    printTasks(tasks)

    // 4. Сохраняем в файл
    err = saveTasks(tasks)
    if err != nil {
        fmt.Println("Ошибка сохранения:", err)
        return
    }

    fmt.Println("Задачи сохранены в", tasksFile)
}

Запустите эту программу, и она создаст файл tasks.json. Запустите второй раз — она загрузит существующие задачи и добавит новые. Это базовый паттерн персистентного хранения данных:

Паттерн «Загрузить → Изменить → Сохранить»:

1. При запуске: loadTasks() — читаем JSON из файла в слайс структур

2. Во время работы: добавляем, изменяем, удаляем элементы слайса

3. При завершении: saveTasks() — записываем слайс обратно в файл как JSON

Обратите внимание на функцию loadTasks. Она проверяет, существует ли файл, с помощью os.IsNotExist(err). Если файла нет — это не ошибка, просто возвращаем пустой слайс. Это делает программу устойчивой при первом запуске.

💡 Совет для финального проекта: Этот паттерн загрузки/сохранения JSON — основа нашей CLI-утилиты. Выделяйте функции чтения и записи в отдельные файлы (storage.go) и используйте их как «слой хранения». Тогда основная логика программы не будет знать, как именно хранятся данные — в JSON, в базе данных или в облаке.

📎 Шпаргалка: основные функции encoding/json

json.Marshal(v) — структура/map/слайс → []byte (JSON в одну строку)

json.MarshalIndent(v, prefix, indent) — то же, но с отступами (для файлов и отладки)

json.Unmarshal(data, &v) — []byte (JSON) → структура/map/слайс

json.NewDecoder(r).Decode(&v) — потоковое чтение из io.Reader (файл, HTTP)

json.NewEncoder(w).Encode(v) — потоковая запись в io.Writer (файл, HTTP)

Теги структур:

`json:"name"` — задать имя поля в JSON

`json:"name,omitempty"` — пропустить, если значение нулевое

`json:"-"` — полностью исключить поле из JSON

📋 Итоги урока

  • JSON — текстовый формат для хранения и передачи данных, стандарт де-факто в веб-разработке
  • json.Marshal превращает данные Go в JSON, json.Unmarshal — разбирает JSON обратно
  • Теги структур (`json:"name"`) задают имена полей, omitempty пропускает нулевые, "-" исключает полностью
  • Для файлов и HTTP: json.NewDecoder / json.NewEncoder работают с потоками
  • os.ReadFile + json.Unmarshal — простейший способ прочитать JSON из файла
  • json.MarshalIndent + os.WriteFile — простейший способ записать JSON в файл
  • При разборе в map[string]any все числа приходят как float64 — помните об этом
  • Паттерн «Загрузить → Изменить → Сохранить» — основа хранения данных в нашем финальном проекте
  • Всегда проверяйте ошибки при маршалинге и анмаршалинге

В следующем уроке 5.3 мы изучим HTTP-запросы в Go: научимся делать GET и POST запросы к реальным API, отправлять заголовки и тело запроса, обрабатывать ответы. Мы подключим нашу CLI-утилиту к внешнему API и объединим знания JSON и HTTP в единый рабочий процесс.

Работа с JSON — Язык Go – для начинающих! | $ sudo teach IT