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-формате, а не через простой текст.
Частые ошибки
- Забыли
&вjson.Unmarshal. - Поля структуры не экспортированы.
- Неправильные теги:
json: "name"вместоjson:"name". - Конфиг прочитали, но не провалидировали.
- Секреты положили в YAML и закоммитили.
- Не ограничили размер JSON body в HTTP handler.
- Использовали
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)