encoding/json: сериализация JSON в Go
Кратко:
encoding/json— стандартный пакет для работы с JSON. Используется в 99% сервисов: REST API, конфиги, логи. Несмотря на простоту API (Marshal/Unmarshal), там куча подводных камней: невидимое поведение для unexported полей, проблемы с числами в JS-клиентах, NaN, performance.Зачем знать джуну: Каждый REST endpoint, который ты напишешь, использует JSON. Спросят про struct tags, custom marshalers, разницу
json.Numbervsfloat64. В production будут баги типа “поле теряется при сериализации” — нужно знать, что unexported не маршалится.
Содержание
Заголовок раздела «Содержание»- Базовое API: Marshal, Unmarshal, Encoder, Decoder
- Под капотом: reflection, struct tags, generics
- Gotchas: типичные ловушки
- Производительность: альтернативы, бенчмарки
- Вопросы на собесе
- Practice
- Источники
1. Базовое API
Заголовок раздела «1. Базовое API»1.1 Marshal и Unmarshal — старт
Заголовок раздела «1.1 Marshal и Unmarshal — старт»package main
import ( "encoding/json" "fmt")
type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"`}
func main() { u := User{ID: 1, Name: "Алиса", Email: "a@b.com"}
// Marshal: struct → JSON data, err := json.Marshal(u) if err != nil { panic(err) } fmt.Println(string(data)) // {"id":1,"name":"Алиса","email":"a@b.com"}
// Unmarshal: JSON → struct var u2 User err = json.Unmarshal(data, &u2) // обязательно указатель! if err != nil { panic(err) } fmt.Printf("%+v\n", u2) // {ID:1 Name:Алиса Email:a@b.com}}⚠️ Unmarshal требует указатель — иначе функция не сможет изменить значение.
1.2 MarshalIndent — pretty print
Заголовок раздела «1.2 MarshalIndent — pretty print»data, _ := json.MarshalIndent(u, "", " ")fmt.Println(string(data))// {// "id": 1,// "name": "Алиса",// "email": "a@b.com"// }Удобно для конфигов или дебага. В сети — лучше компактный Marshal.
1.3 Struct tags
Заголовок раздела «1.3 Struct tags»type Product struct { ID int `json:"id"` Name string `json:"name"` Price float64 `json:"price,omitempty"` // не сериализовать если 0 Internal string `json:"-"` // никогда не сериализовать SKU string `json:"sku,string"` // как строка, даже если число Notes string `json:",omitempty"` // имя по умолчанию = "Notes"}| Тег | Что делает |
|---|---|
json:"name" | Использовать “name” вместо имени поля |
json:"name,omitempty" | Пропустить, если zero value |
json:"-" | Никогда не сериализовать |
json:",omitempty" | Имя по умолчанию + omit empty |
json:"name,string" | Сериализовать число как строку |
omitempty — что считается empty?
Заголовок раздела «omitempty — что считается empty?»0для чисел.""для строк.nilдля указателей, слайсов, мап, интерфейсов.falseдля bool.- НЕ empty: пустой не-nil слайс
[]int{}, пустая не-nil мапаmap[string]int{}, struct (даже если все поля нулевые).
⚠️ Поэтому omitempty не работает для struct полей. Чтобы пропускать nullable struct — используй указатель: *Address вместо Address.
1.4 Encoder/Decoder для streams
Заголовок раздела «1.4 Encoder/Decoder для streams»// Encoder — пишет в io.Writerenc := json.NewEncoder(w)enc.SetIndent("", " ")enc.SetEscapeHTML(false)enc.Encode(u)
// Decoder — читает из io.Readerdec := json.NewDecoder(r)dec.DisallowUnknownFields() // strict modeerr := dec.Decode(&u)Когда использовать:
Marshal/Unmarshal— для маленьких объектов в памяти.Encoder/Decoder— для streams (HTTP body, файлы, NDJSON).
NDJSON / JSON Lines
Заголовок раздела «NDJSON / JSON Lines»dec := json.NewDecoder(file)for dec.More() { var item Item if err := dec.Decode(&item); err != nil { return err } // обработать item}Удобно для логов и больших файлов — не грузим всё в память.
1.5 Поля должны быть EXPORTED!
Заголовок раздела «1.5 Поля должны быть EXPORTED!»type User struct { name string `json:"name"` // unexported → НЕ маршалится! (silent) Age int `json:"age"` // exported → ок}
u := User{name: "Bob", Age: 30}data, _ := json.Marshal(u)fmt.Println(string(data))// {"age":30} ← name пропал!⚠️ Это молчаливый баг. Никаких предупреждений. Если поле начинается со строчной буквы — json пакет его не видит (reflect не может прочитать unexported).
1.6 Nil slices/maps в JSON
Заголовок раздела «1.6 Nil slices/maps в JSON»type S struct { A []int `json:"a"` B map[string]int `json:"b"`}
s1 := S{} // nil slice, nil mapdata, _ := json.Marshal(s1)fmt.Println(string(data))// {"a":null,"b":null}
s2 := S{A: []int{}, B: map[string]int{}}data, _ = json.Marshal(s2)fmt.Println(string(data))// {"a":[],"b":{}}Разница важна для клиентов: JS-клиент может различать null и [].
1.7 Generic JSON через map[string]interface{}
Заголовок раздела «1.7 Generic JSON через map[string]interface{}»Если структура заранее неизвестна:
var m map[string]interface{}err := json.Unmarshal(data, &m)// или:var m map[string]any // Go 1.18+Какие типы получаются?
Заголовок раздела «Какие типы получаются?»| JSON | Go (через interface{}) |
|---|---|
string | string |
number | float64 (!) |
bool | bool |
null | nil |
array | []interface{} |
object | map[string]interface{} |
⚠️ Все числа → float64! Даже целые. Это создаёт проблемы:
data := []byte(`{"id": 12345678901234567890}`)var m map[string]interface{}json.Unmarshal(data, &m)fmt.Println(m["id"]) // 1.2345678901234567e+19 — потеря точностиРешения:
- Использовать
json.Number(см. ниже). - Парсить в конкретный struct.
1.8 json.Number — точные числа
Заголовок раздела «1.8 json.Number — точные числа»dec := json.NewDecoder(bytes.NewReader(data))dec.UseNumber()
var m map[string]interface{}dec.Decode(&m)
n := m["id"].(json.Number)intVal, err := n.Int64()floatVal, err := n.Float64()stringVal := n.String() // оригинальная строкаjson.Number — это просто type Number string. Хранит число как строку, конвертирует по требованию. Не теряет точность.
1.9 Числа как строки — ,string tag
Заголовок раздела «1.9 Числа как строки — ,string tag»Для денег, ID, и других “точных” значений лучше слать как строки (особенно для JS-клиентов — JS Number теряет точность после 2^53):
type Order struct { ID int64 `json:"id,string"` // {"id": "12345"} Total float64 `json:"total,string"` // {"total": "99.99"}}При маршалинге пишет в строку, при анмаршалинге парсит обратно.
1.10 Custom Marshaler / Unmarshaler
Заголовок раздела «1.10 Custom Marshaler / Unmarshaler»Если нужно нестандартное поведение — реализуй интерфейсы:
type Marshaler interface { MarshalJSON() ([]byte, error)}
type Unmarshaler interface { UnmarshalJSON([]byte) error}Пример: формат даты:
type Date struct { time.Time}
func (d Date) MarshalJSON() ([]byte, error) { return []byte(`"` + d.Format("2006-01-02") + `"`), nil}
func (d *Date) UnmarshalJSON(data []byte) error { s := strings.Trim(string(data), `"`) t, err := time.Parse("2006-01-02", s) if err != nil { return err } d.Time = t return nil}⚠️ Аккуратно с указателями vs значениями:
func (d Date) MarshalJSON()— будет вызван и дляDate, и для*Date.func (d *Date) UnmarshalJSON()— нужен указатель, чтобы изменять.
1.11 json.RawMessage — отложенный парсинг
Заголовок раздела «1.11 json.RawMessage — отложенный парсинг»type Envelope struct { Type string `json:"type"` Payload json.RawMessage `json:"payload"`}
func main() { data := []byte(`{"type":"user","payload":{"id":1,"name":"Alice"}}`)
var env Envelope json.Unmarshal(data, &env)
switch env.Type { case "user": var u User json.Unmarshal(env.Payload, &u) // ... case "order": var o Order json.Unmarshal(env.Payload, &o) }}RawMessage — это []byte, но сохраняется как-есть и не парсится. Идеален для discriminated unions (type field + payload).
1.12 Embedded structs
Заголовок раздела «1.12 Embedded structs»type Base struct { ID int `json:"id"` CreatedAt time.Time `json:"created_at"`}
type User struct { Base Name string `json:"name"`}
u := User{Base: Base{ID: 1, CreatedAt: time.Now()}, Name: "Alice"}data, _ := json.Marshal(u)// {"id":1,"created_at":"...","name":"Alice"}Поля embedded struct “вмержены” в JSON. Это работает только если embedded — anonymous field (без имени).
Если хочешь, чтобы был вложенный объект:
type User struct { Meta Base `json:"meta"` Name string `json:"name"`}// {"meta":{"id":1,...},"name":"Alice"}1.13 DisallowUnknownFields — strict mode
Заголовок раздела «1.13 DisallowUnknownFields — strict mode»dec := json.NewDecoder(r)dec.DisallowUnknownFields()err := dec.Decode(&user)// если в JSON есть поле, которого нет в struct → ошибкаХорошо для API, чтобы ловить опечатки клиентов. По умолчанию лишние поля игнорируются.
1.14 HTML escaping
Заголовок раздела «1.14 HTML escaping»По умолчанию <, >, & экранируются в <, >, & — защита от XSS если JSON вставляется в HTML.
data, _ := json.Marshal(map[string]string{"q": "a < b"})fmt.Println(string(data))// {"q":"a < b"}Для API без HTML-контекста — отключи:
enc := json.NewEncoder(w)enc.SetEscapeHTML(false)enc.Encode(v)// {"q":"a < b"}1.15 time.Time — стандарт RFC3339
Заголовок раздела «1.15 time.Time — стандарт RFC3339»type Event struct { Time time.Time `json:"time"`}
e := Event{Time: time.Now()}data, _ := json.Marshal(e)// {"time":"2026-05-21T15:04:05.123456789Z"}Для кастомного формата — Custom Marshaler (см. 1.10) или wrapper-тип.
⚠️ Подвох: time.Time маршалится в RFC3339Nano, но часто клиенты ждут unix timestamp. Сделай отдельный wrapper-тип, если нужно.
2. Под капотом
Заголовок раздела «2. Под капотом»2.1 Reflection
Заголовок раздела «2.1 Reflection»encoding/json использует пакет reflect для прохода по полям структуры в runtime. Это удобно (один Marshal на всё), но медленно относительно генерированного кода.
Marshal(v interface{}): 1. reflect.ValueOf(v) → получаем Value 2. reflect.TypeOf(v) → получаем Type 3. Для каждого поля Type.Field(i): - читаем tag через field.Tag.Get("json") - читаем значение через value.Field(i) - рекурсивно маршалим 4. Кешируем структуру (encode caching) 5. Пишем в bytes.BufferПосле первого Marshal структуры — её “схема” кешируется. Поэтому второй вызов быстрее.
2.2 Struct tags парсинг
Заголовок раздела «2.2 Struct tags парсинг»field, ok := reflect.TypeOf(u).Elem().FieldByName("Name")tag := field.Tag.Get("json") // "name,omitempty"Пакет json сам парсит эту строку: имя поля и опции через запятую.
2.3 Регистрозависимость? Нет!
Заголовок раздела «2.3 Регистрозависимость? Нет!»data := []byte(`{"NAME": "Alice"}`)type User struct { Name string `json:"name"`}var u Userjson.Unmarshal(data, &u)fmt.Println(u.Name) // "Alice"При unmarshal Go сначала ищет точное совпадение, потом case-insensitive. Это упрощает работу с разными источниками, но может скрыть опечатки.
⚠️ Marshal всегда пишет точно как в теге.
2.4 Что попадает в JSON?
Заголовок раздела «2.4 Что попадает в JSON?»| Тип Go | JSON |
|---|---|
bool | true/false |
Целые (int, int64, …) | number |
Float (float32, float64) | number |
string | string |
[]byte | base64-кодированная string |
time.Time | RFC3339 string |
*T | null или значение |
[]T | array |
map[string]T | object |
struct | object |
interface{} | по типу значения |
| nil slice/map | null |
| Каналы, функции | ошибка |
2.5 NaN, +Inf, -Inf — ошибка!
Заголовок раздела «2.5 NaN, +Inf, -Inf — ошибка!»data, err := json.Marshal(math.NaN())fmt.Println(err) // json: unsupported value: NaNJSON не имеет специальных значений для NaN/Inf. Если у тебя в данных может быть NaN — придётся обработать самому (заменить на null или строку через Custom Marshaler).
2.6 Уникальность ключей в map
Заголовок раздела «2.6 Уникальность ключей в map»Map в Go — unordered. При Marshal ключи сортируются для детерминированного вывода (с Go 1.12+). Это полезно для тестов и diff’ов.
m := map[string]int{"b": 2, "a": 1, "c": 3}data, _ := json.Marshal(m)// {"a":1,"b":2,"c":3} ← всегда отсортировано2.7 Map keys — должны быть string или textMarshaler
Заголовок раздела «2.7 Map keys — должны быть string или textMarshaler»m := map[int]string{1: "a"}data, _ := json.Marshal(m)// {"1":"a"} ← int → string ОК (с Go 1.7+)
type ID intm2 := map[ID]string{1: "a"}data, _ = json.Marshal(m2) // тоже работает
m3 := map[struct{ X int }]string{{1}: "a"}data, _ = json.Marshal(m3) // ОШИБКА: unsupported typeТолько числовые, строковые типы и реализующие encoding.TextMarshaler могут быть ключами.
2.8 Декодирование во вложенные мапы
Заголовок раздела «2.8 Декодирование во вложенные мапы»data := []byte(`{"a":{"b":{"c":1}}}`)var m map[string]interface{}json.Unmarshal(data, &m)
// m["a"] — это map[string]interface{}// m["a"].(map[string]interface{})["b"]...Каждый уровень — отдельная аллокация. Очень медленно и больно для больших JSON. Используй конкретные структуры или RawMessage.
3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ 3.1 Unexported поля silently ignored
Заголовок раздела «⚠️ 3.1 Unexported поля silently ignored»type S struct { name string `json:"name"` // strolling case → INVISIBLE Age int `json:"age"`}json.Marshal(S{name: "x", Age: 1}) → {"age":1}. Никаких ошибок!
Правило: все поля для JSON — с заглавной.
⚠️ 3.2 Tag без json:
Заголовок раздела «⚠️ 3.2 Tag без json:»type S struct { Name string `foo:"bar"` // нет json: → имя поля "Name"}
s := S{Name: "x"}data, _ := json.Marshal(s)// {"Name":"x"} ← НЕ "name"!Если пишешь json:"-" чтобы пропустить — ОК. Но json:"-," (с запятой) — это поле с именем -!
type S struct { X int `json:"-,"` // имя поля "-"!}data, _ := json.Marshal(S{X: 1})// {"-":1}⚠️ 3.3 omitempty не работает для struct
Заголовок раздела «⚠️ 3.3 omitempty не работает для struct»type Address struct { City string `json:"city"`}type User struct { Name string `json:"name"` Address Address `json:"address,omitempty"` // НЕ пропустится!}
u := User{Name: "Alice"} // Address — zero valuedata, _ := json.Marshal(u)// {"name":"Alice","address":{"city":""}}Решение: использовать указатель:
Address *Address `json:"address,omitempty"`⚠️ 3.4 Marshal map с не-string ключами
Заголовок раздела «⚠️ 3.4 Marshal map с не-string ключами»m := map[int]string{1: "a", 2: "b"}data, _ := json.Marshal(m)// {"1":"a","2":"b"} ← числа конвертируются в строкиЭто работает с Go 1.7+. До этого — ошибка. Для произвольных типов нужен encoding.TextMarshaler.
⚠️ 3.5 Float overflow при Unmarshal в int
Заголовок раздела «⚠️ 3.5 Float overflow при Unmarshal в int»data := []byte(`{"x": 12345678901234567890}`)var s struct{ X int }err := json.Unmarshal(data, &s)fmt.Println(err) // json: cannot unmarshal number ... into Go struct fieldЕсли число не помещается в int — ошибка.
Но float → int с потерей молча:
data := []byte(`{"x": 1.5}`)err := json.Unmarshal(data, &s) // error: ... into Go value of type int⚠️ 3.6 NaN/Inf не сериализуются
Заголовок раздела «⚠️ 3.6 NaN/Inf не сериализуются»type Stats struct { Avg float64 `json:"avg"`}s := Stats{Avg: math.NaN()}_, err := json.Marshal(s)fmt.Println(err) // unsupported value: NaNПроверяй: math.IsNaN(v), math.IsInf(v, 0). Заменяй на 0 или null.
⚠️ 3.7 Большие числа в JS-клиентах
Заголовок раздела «⚠️ 3.7 Большие числа в JS-клиентах»JavaScript Number — это float64. Точность теряется после 2^53 ≈ 9e15.
{"id": 9007199254740993}JS прочитает это как 9007199254740992 — последняя цифра потеряна!
Решение: ID-шники как строки:
ID int64 `json:"id,string"` // {"id": "9007199254740993"}⚠️ 3.8 HTML-escape в API
Заголовок раздела «⚠️ 3.8 HTML-escape в API»data, _ := json.Marshal(map[string]string{"q": "a < b"})// {"q":"a < b"}Если ты пишешь в HTML — это безопасно. В API — ужасно (увеличивает размер, нечитаемо).
Решение: Encoder.SetEscapeHTML(false) или ручная замена.
⚠️ 3.9 Trailing newline в Encoder.Encode
Заголовок раздела «⚠️ 3.9 Trailing newline в Encoder.Encode»enc := json.NewEncoder(w)enc.Encode(v) // добавляет \n в конце!Если кому-то это мешает (например, парсер не любит) — используй Marshal + ручной Write.
⚠️ 3.10 Decoder не читает до конца input’а
Заголовок раздела «⚠️ 3.10 Decoder не читает до конца input’а»data := []byte(`{"a":1}{"b":2}`) // два JSONdec := json.NewDecoder(bytes.NewReader(data))
var x map[string]intdec.Decode(&x) // прочитал только {"a":1}fmt.Println(x) // map[a:1]
dec.Decode(&x) // следующий объектfmt.Println(x) // map[b:2]Это фича для NDJSON. Но json.Unmarshal требует, чтобы input был одним JSON-значением без лишнего.
⚠️ 3.11 Время marshalling — нюансы
Заголовок раздела «⚠️ 3.11 Время marshalling — нюансы»t := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)data, _ := json.Marshal(t)fmt.Println(string(data))// "2026-01-01T00:00:00Z"- Если у
time.Timeнет nanoseconds —Z(UTC) или офсет (+03:00). - Если есть наносекунды —
2026-01-01T00:00:00.123456789Z.
Для разных нужд (unix timestamp, custom format) — оборачивай в свой тип.
⚠️ 3.12 Pointer fields могут быть nil
Заголовок раздела «⚠️ 3.12 Pointer fields могут быть nil»type User struct { Name *string `json:"name"`}
u := User{} // Name == nildata, _ := json.Marshal(u)// {"name":null}
// При Unmarshal без значения:json.Unmarshal([]byte(`{}`), &u)fmt.Println(u.Name) // nil, не панику!Используй pointer fields для опциональных значений (различать “нет” vs “пустая строка”).
⚠️ 3.13 Decoder возвращает io.EOF
Заголовок раздела «⚠️ 3.13 Decoder возвращает io.EOF»dec := json.NewDecoder(strings.NewReader(""))var v interface{}err := dec.Decode(&v)fmt.Println(err) // EOFУчитывай, что пустой input — это EOF, а не валидный JSON.
⚠️ 3.14 Анмаршал в interface{} даёт float64 для чисел
Заголовок раздела «⚠️ 3.14 Анмаршал в interface{} даёт float64 для чисел»Уже упоминалось, но повторим:
var m map[string]interface{}json.Unmarshal([]byte(`{"id":42}`), &m)id := m["id"].(int) // PANIC! interface conversion: interface {} is float64// Нужно: id := int(m["id"].(float64))4. Производительность
Заголовок раздела «4. Производительность»4.1 Бенчмарк стандартного encoding/json
Заголовок раздела «4.1 Бенчмарк стандартного encoding/json»type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"`}
func BenchmarkMarshal(b *testing.B) { u := User{ID: 1, Name: "Alice", Email: "a@b.com"} b.ReportAllocs() for i := 0; i < b.N; i++ { json.Marshal(u) }}BenchmarkMarshal-8 3000000 400 ns/op 96 B/op 2 allocs/opНе быстро. Но для большинства задач — достаточно.
4.2 Encoder vs Marshal
Заголовок раздела «4.2 Encoder vs Marshal»Для streams — Encoder быстрее (один буфер, не создаёт промежуточный []byte):
// БАГ: лишняя аллокацияdata, _ := json.Marshal(v)w.Write(data)
// OK:json.NewEncoder(w).Encode(v)4.3 Альтернативы для performance
Заголовок раздела «4.3 Альтернативы для performance»| Библиотека | Скорость | Особенности |
|---|---|---|
encoding/json | 1x | stdlib, на reflect, везде работает |
json-iterator/go | 2-4x | drop-in replacement, тот же API |
easyjson | 4-6x | требует codegen (easyjson -all file.go) |
bytedance/sonic | 8-10x | использует assembly, JIT, нужны определённые CPU |
goccy/go-json | 3-5x | drop-in replacement |
valyala/fastjson | 10-20x | не-стандартный API, парсер по path |
Для джуна: знай о существовании, но не используй без необходимости. stdlib хватает для 95% задач.
4.4 Pre-allocation
Заголовок раздела «4.4 Pre-allocation»// БАГ: bytes.Buffer без капаситиvar buf bytes.Bufferenc := json.NewEncoder(&buf)enc.Encode(v)
// OK: заранее выделяемbuf := bytes.NewBuffer(make([]byte, 0, 1024))4.5 sync.Pool для буферов
Заголовок раздела «4.5 sync.Pool для буферов»var encoderPool = sync.Pool{ New: func() any { return new(bytes.Buffer) },}
func marshal(v interface{}) ([]byte, error) { buf := encoderPool.Get().(*bytes.Buffer) buf.Reset() defer encoderPool.Put(buf)
enc := json.NewEncoder(buf) if err := enc.Encode(v); err != nil { return nil, err } out := make([]byte, buf.Len()) copy(out, buf.Bytes()) return out, nil}4.6 Когда переходить на быстрые библиотеки?
Заголовок раздела «4.6 Когда переходить на быстрые библиотеки?»- Сериализация — узкое место в профиле (>20% CPU).
- Тысячи RPS с большими JSON-объектами.
- Большие batch-обработки.
Раньше не оптимизируй. Сначала измерь.
4.7 Codegen с easyjson
Заголовок раздела «4.7 Codegen с easyjson»go install github.com/mailru/easyjson/...@latesteasyjson -all user.goСгенерирует user_easyjson.go с быстрыми MarshalJSON/UnmarshalJSON. Эти методы будут использоваться вместо reflect.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»Базовые
Заголовок раздела «Базовые»1. Что такое json.Marshal и json.Unmarshal? Marshal — Go-значение в JSON []byte. Unmarshal — JSON []byte в Go-значение (нужен указатель).
2. Почему Unmarshal требует указатель? Чтобы изменять значение по адресу. Без указателя функция работает с копией.
3. Что делает omitempty? Пропускает поле, если оно zero value. Для чисел — 0, строк — "", указателей/слайсов/мап — nil, bool — false.
4. Почему unexported поля не маршалятся? Пакет json использует reflect, который не может читать unexported поля чужих пакетов (ограничение Go).
5. Что такое struct tags?
Строковые метаданные на полях, формат key:"value". Пакет json читает json:"..." и применяет.
6. Какие опции в json теге есть?
json:"name" — имя, ,omitempty — пропускать zero, ,string — как строку, json:"-" — никогда не сериализовать.
7. Что произойдёт при Unmarshal в interface{}? Числа → float64, строки → string, объекты → map[string]interface{}, массивы → []interface{}, null → nil, bool → bool.
8. Почему числа становятся float64? JSON не различает int и float — это все “number”. Без типовой информации Go использует float64 по умолчанию.
9. Что делать с большими числами?
json.Number(черезDecoder.UseNumber()).- Парсить в конкретный struct с int64.
- Для клиентов на JS — слать как строку через
,string.
10. Что такое json.RawMessage?
Тип []byte, который не парсится при Unmarshal/Marshal. Полезен для отложенного парсинга (discriminated unions).
Средние
Заголовок раздела «Средние»11. Как сделать custom формат для time.Time?
Wrapper-тип с методами MarshalJSON() и UnmarshalJSON().
12. Почему omitempty не работает для struct?
Пустой struct не считается empty (zero value есть, но не nil). Используй указатель *Address вместо Address.
13. Чем Encoder отличается от Marshal?
Encoder пишет в io.Writer (без промежуточного []byte) и поддерживает streaming. Marshal возвращает []byte.
14. Как сделать strict-режим (запретить unknown fields)?
dec.DisallowUnknownFields()15. Что делает SetEscapeHTML(false)?
Не экранирует <, >, & в < и т.д. Для API-ответов часто отключают.
16. Можно ли иметь циклические ссылки?
Нет, Marshal уйдёт в бесконечную рекурсию. Нужно вручную ломать циклы (например, MarshalJSON, который не сериализует обратную ссылку).
17. Как обработать NaN/Inf?
По умолчанию — ошибка. Custom Marshaler может вернуть null или другую замену.
18. Что такое embedded struct в JSON? Anonymous fields “вмерживаются” — их поля попадают в верхний уровень JSON.
Продвинутые
Заголовок раздела «Продвинутые»19. Как ускорить JSON?
- Использовать
json-iterator/go,easyjson,sonic,go-json. - sync.Pool для буферов.
- Прекомпилированные структуры (Codegen).
- Уменьшить размер JSON (omitempty, короткие имена).
20. Что такое MarshalJSON для интерфейса?
Если тип реализует MarshalJSON() ([]byte, error), пакет json вызовет его вместо стандартной сериализации.
21. Map с не-string ключами? С Go 1.7+ числовые ключи конвертируются в строки. Произвольные типы — только через TextMarshaler.
22. Сортируются ли ключи map? Да, с Go 1.12+ Marshal сортирует ключи map для детерминированного вывода.
23. Decoder.More()? Используется в цикле для NDJSON (несколько JSON в одном потоке). Возвращает true, пока есть ещё данные.
24. Как валидировать input?
DisallowUnknownFieldsдля strict.- Кастомный UnmarshalJSON с проверками.
- Сторонние библиотеки (go-playground/validator) после Unmarshal.
25. Чем easyjson отличается от sonic?
- easyjson: codegen, нужен build step, кросс-платформенный.
- sonic: использует assembly и JIT, работает на amd64/arm64 Linux/macOS, быстрее, но “магический”.
6. Practice
Заголовок раздела «6. Practice»Задача 1: Discriminated union
Заголовок раздела «Задача 1: Discriminated union»Тебе приходит JSON с полями type и data. В зависимости от type нужно парсить data в разные структуры:
{"type": "user", "data": {"id": 1, "name": "Alice"}}{"type": "order", "data": {"id": 100, "total": 50.0}}Напиши парсер через json.RawMessage.
Задача 2: Custom date format
Заголовок раздела «Задача 2: Custom date format»Сделай тип Date (только год-месяц-день) с поддержкой формата 2006-01-02 в JSON. Покрой тестами marshal/unmarshal.
Задача 3: Optional fields
Заголовок раздела «Задача 3: Optional fields»Опиши структуру User со следующими опциональными полями:
email— может отсутствовать или быть"".age— может отсутствовать или быть 0.address— целая структура, опциональная.
Сделай так, чтобы JSON правильно различал “не пришло” и “пришло пустое”.
Задача 4: Strict API endpoint
Заголовок раздела «Задача 4: Strict API endpoint»Напиши HTTP handler, который принимает POST JSON. При наличии лишних полей возвращает 400 с описанием. Используй DisallowUnknownFields.
Задача 5: Стримлайн NDJSON
Заголовок раздела «Задача 5: Стримлайн NDJSON»Напиши функцию Process(r io.Reader), которая читает NDJSON построчно и обрабатывает каждый объект. Не грузи весь файл в память.
Задача 6: Бенчмарк альтернатив
Заголовок раздела «Задача 6: Бенчмарк альтернатив»Сравни encoding/json и json-iterator/go на структуре с 10 полями. Замерь Marshal и Unmarshal, B/op, allocs/op.
Задача 7: Найди баг
Заголовок раздела «Задача 7: Найди баг»type Product struct { name string `json:"name"` Price int `json:"price,omitempty"`}
p := Product{name: "Apple", Price: 0}data, _ := json.Marshal(p)fmt.Println(string(data))Что выведет? Почему?
7. Источники
Заголовок раздела «7. Источники»- Документация encoding/json — https://pkg.go.dev/encoding/json
- JSON and Go (Go Blog) — https://go.dev/blog/json
- JSON best practices in Go — https://www.alexedwards.net/blog/json-tips-and-tricks
- easyjson — https://github.com/mailru/easyjson
- sonic — https://github.com/bytedance/sonic
- json-iterator — https://github.com/json-iterator/go
- 100 Go Mistakes — Teiva Harsanyi (глава про JSON).
Чек-лист джуна:
- Поля для JSON всегда с заглавной (exported).
- Знаю про struct tags:
json:"name,omitempty". - Понимаю, что omitempty не работает для struct (нужен pointer).
- Использую
json.Numberдля точных чисел. - Для больших ID шлю строки (
,stringили string-тип). - Знаю, что числа в
interface{}→ float64. - Использую Encoder для streams и больших объектов.
- Знаю про DisallowUnknownFields для строгости.
- Не забываю SetEscapeHTML(false) для API.
- Custom Marshaler для time.Time в нестандартном формате.