Содержание

JSON, YAML и struct tags в Go

Большинство backend-программ постоянно превращают данные из одного вида в другой. HTTP API получает JSON и превращает его в Go-структуру. Программа читает YAML-конфиг и превращает его в настройки. CLI сохраняет отчет в файл. Сервис отправляет объект во внешний API.

JSON и YAML - это форматы обмена данными. Go-структуры - это то, как эти данные живут внутри программы. Struct tags связывают эти два мира.

JSON как договор

JSON выглядит как объект из ключей и значений:

{ "id": 1, "name": "Alice", "email": "alice@example.com" }

Для Go это удобно представить структурой:

type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` }

Поля структуры написаны с большой буквы, потому что пакет encoding/json видит только экспортируемые поля. Поле name string не попадет в JSON, даже если у него есть tag.

Struct tags

Tag после поля говорит, каким именем это поле должно называться в JSON:

type User struct { ID int `json:"id"` FullName string `json:"full_name"` Email string `json:"email,omitempty"` }

omitempty пропускает поле, если значение пустое:

  • "" для строки;
  • 0 для числа;
  • false для bool;
  • nil для slice, map, pointer.

Это полезно, когда поле необязательное.

Marshal

json.Marshal превращает Go-значение в JSON:

user := User{ID: 1, FullName: "Alice"} data, err := json.Marshal(user) if err != nil { return fmt.Errorf("encode user: %w", err) } fmt.Println(string(data))

Результат:

{"id":1,"full_name":"Alice"}

Для красивого вывода используйте MarshalIndent:

data, err := json.MarshalIndent(user, "", " ")

Такой JSON удобнее читать человеку, например в CLI-отчете или debug-файле.

Unmarshal

json.Unmarshal читает JSON в структуру:

var user User err := json.Unmarshal(data, &user) if err != nil { return fmt.Errorf("decode user: %w", err) }

Обратите внимание на &user. Функция должна изменить переменную, поэтому ей нужен указатель. Если передать user без &, декодер не сможет записать результат.

Списки и вложенные структуры

JSON часто содержит массивы и вложенные объекты:

type Article struct { Title string `json:"title"` Tags []string `json:"tags"` Author User `json:"author"` }

Если структура ответа большая, не обязательно описывать все поля. Go спокойно игнорирует лишние ключи в JSON:

type GitHubRepo struct { Name string `json:"name"` Description string `json:"description"` Stars int `json:"stargazers_count"` }

Это удобно при интеграции с внешними API: описывайте только то, что реально используете.

Decoder и Encoder

Для HTTP-ответов, файлов и потоков удобно использовать json.Decoder:

defer resp.Body.Close() var result APIResponse decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&result); err != nil { return fmt.Errorf("decode response: %w", err) }

Так не нужно сначала читать все тело в []byte. Декодер читает из io.Reader.

Для записи в io.Writer используйте json.Encoder:

encoder := json.NewEncoder(w) if err := encoder.Encode(response); err != nil { return err }

В HTTP handler это особенно удобно.

Unknown fields

По умолчанию json.Decoder игнорирует неизвестные поля. Для строгих API иногда лучше включить проверку:

decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return }

Так клиент сразу узнает, что отправил поле, которое API не поддерживает.

YAML

YAML часто используют для конфигурации: Docker Compose, Kubernetes, CI, настройки CLI. В стандартной библиотеке Go нет YAML, но часто используют gopkg.in/yaml.v3.

type Config struct { Port int `yaml:"port"` Host string `yaml:"host"` } data, err := os.ReadFile("config.yaml") if err != nil { return fmt.Errorf("read config: %w", err) } var config Config if err := yaml.Unmarshal(data, &config); err != nil { return fmt.Errorf("parse config: %w", err) }

Пример YAML:

port: 8080 host: localhost

YAML удобен человеку, но чувствителен к отступам. Если конфиг не парсится, сначала проверьте пробелы и уровни вложенности.

Конфиги

Конфиг нужно не только прочитать, но и проверить.

func (c Config) Validate() error { if c.Port == 0 { return errors.New("port is required") } if c.Host == "" { return errors.New("host is required") } return nil }

Значения по умолчанию лучше задавать явно:

if config.Port == 0 { config.Port = 8080 }

Секреты лучше хранить не в YAML, а в переменных окружения:

config.DatabaseURL = os.Getenv("DATABASE_URL") if config.DatabaseURL == "" { return errors.New("DATABASE_URL is required") }

Так меньше риск случайно закоммитить пароль или API-ключ.

JSON в HTTP handler

Типичный handler читает JSON, валидирует его и возвращает JSON:

func createUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&req); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } if req.Email == "" { http.Error(w, "email is required", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }

В реальном проекте ошибки лучше возвращать в едином JSON-формате, а не через простой текст.

Частые ошибки

  1. Забыли & в json.Unmarshal.
  2. Поля структуры не экспортированы.
  3. Неправильные теги: json: "name" вместо json:"name".
  4. Конфиг прочитали, но не провалидировали.
  5. Секреты положили в YAML и закоммитили.
  6. Не ограничили размер JSON body в HTTP handler.
  7. Использовали map[string]any там, где структура была бы понятнее.

Шпаргалка

data, err := json.Marshal(value) data, err := json.MarshalIndent(value, "", " ") err := json.Unmarshal(data, &value) decoder := json.NewDecoder(reader) err := decoder.Decode(&value) encoder := json.NewEncoder(writer) err := encoder.Encode(value)

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи