MEASUREMENT
bench/gojson/internals.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/json
- How to run it
go run bench/gojson/internals.go bash bench/gojson/recursion.sh go run bench/gojson/cost.go go run bench/gojson/practice.go go run bench/gojson/strict.go GOEXPERIMENT=jsonv2 /usr/local/go1.25.1/bin/go run bench/gojson/jsonv2.go
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «JSON в Go»
| Файл | Что делает |
|---|---|
internals.go |
одиннадцать наблюдений без единого замера времени: одна структура без тегов, с тегами и обратно через Unmarshal; что пропускают omitempty и omitzero для полей тринадцати видов; регистр ключей, лишние ключи и DisallowUnknownFields; Unmarshal в заполненную структуру; большое целое в any, UseNumber и ,string; формы значений — nil-срез, []byte, порядок ключей карты, экранирование HTML, NaN; MarshalJSON на указателе; MarshalJSON с типом-двойником; двойник при встроенном time.Time; наивная формула Cents на отрицательных суммах и исправленная; число вне int64 — ошибка в поле, округление в any |
recursion.sh |
собирает и запускает cases/recursion и cases/alias, печатает сообщение рантайма, код выхода и один виток цикла из трассы стека |
cases/recursion/main.go |
подопытная программа к recursion.sh: MarshalJSON зовёт json.Marshal(e); намеренно падает |
cases/alias/main.go |
подопытная программа к recursion.sh: двойник объявлен псевдонимом type event = Event; намеренно падает |
cost.go |
цена одной структуры из пяти полей: json.Marshal, ручная сборка той же строки через strconv.Append*, json.Unmarshal в структуру и в map[string]any; наносекунды, аллокации, байты и кратности, лучший из 15 кругов |
jsonv2.go |
encoding/json и encoding/json/v2 на одних входах: регистр ключей, nil-срез и карта, omitempty у числа, MarshalJSON на указателе, null в заполненное поле. Только go1.25+ и только с GOEXPERIMENT=jsonv2 |
strict.go |
полный короткий пример (комментарии и вывод по-английски — файл показан в обеих локалях): обработчик HTTP со строгим разбором тела — MaxBytesReader, DisallowUnknownFields, второй Decode обязан вернуть io.EOF; прогон на восьми телах через httptest |
practice.go |
источник ответов двух практических задач урока |
go run bench/gojson/internals.go
bash bench/gojson/recursion.sh
go run bench/gojson/cost.go
go run bench/gojson/practice.go
go run bench/gojson/strict.go
GOEXPERIMENT=jsonv2 /usr/local/go1.25.1/bin/go run bench/gojson/jsonv2.go
Записи прогонов — в runs/, go1.24.7 linux/amd64, 2 ядра.
internals-go1.25.txt—internals.goна go1.25.1: вывод тот же, кроме строки версии.internals-go1.25-jsonv2.txt— он же на go1.25.1 сGOEXPERIMENT=jsonv2. В этом режимеencoding/jsonреализован поверх v2; поведение во всех всех разделах то же, отличаются тексты трёх ошибок (раздел 4, «тип не тот», раздел 5,,stringпри числе во входе, и раздел 11, число внеint64).jsonv2-go1.25.txt—jsonv2.go, go1.25.1,GOEXPERIMENT=jsonv2.strict.txt,strict-go1.25.txt—strict.goна go1.24.7 и go1.25.1: вывод тот же, кроме строки версии.
Документация и release notes сверены 14.10.2026. На эту дату вышли Go 1.26 и
1.27; на них скрипты не запускались. По release notes Go 1.27
encoding/json работает на реализации v2 (прежняя — GOEXPERIMENT=nojsonv2),
поэтому числа cost.go относятся только к go1.24.7 и старой реализации.
Времена в cost.go и practice.go сняты на общей машине, где параллельно
шли другие задачи: абсолютные наносекунды гуляют от запуска к запуску,
устойчивее кратности. Круги способов чередуются, чтобы фоновая нагрузка
доставалась всем поровну.
Что получилось
Видны только экспортируемые поля, и без тегов имена — как в Go.
{"ID":7,"Name":"Ann","Email":"","Tags":null,"Address":{"city":""}}: поля
password нет, пустая строка и nil-срез на месте. Исключение —
неэкспортируемая встроенная структура: Outer{inner{X: 1}, 2} даёт
{"X":1,"Y":2}.
omitempty не пропускает структуры. Пропущены false, 0, "",
nil-указатель, nil-интерфейс, nil- и пустые срез и карта. Не пропущены
указатель на ноль ({"v":0}), массив [2]int{} ({"v":[0,0]}), пустая
структура, Address{} и time.Time{} ({"v":"0001-01-01T00:00:00Z"}).
omitzero (в go1.24.7 он есть) пропускает массив, структуры и time.Time{},
но оставляет пустой не-nil срез и карту.
Unmarshal не смотрит на регистр и молчит о лишнем. "name", "Name",
"NAME", "nAmE" — все заполнили Name. Лишние ключи пропущены без
ошибки; Decoder.DisallowUnknownFields даёт json: unknown field "role".
Ключ "password" при теге "-" тоже пропущен — значение поля не тронуто.
Unmarshal в заполненную структуру — слияние, а не замена. Поле, которого
нет во входе, осталось прежним; null для string ничего не изменил; карта
дополнена (map[likes:5 visits:3]). Срез обрезан до нуля и дописан в тот же
массив: старый срез old после разбора ["z"] показывает [z b c], а
элемент-структура слит со старым — [{A:5 B:2}] из [{A:1 B:2} {A:3 B:4}]
при входе [{"a":5}]. При несовпадении типа
поле пропущено, остальные заполнены, ошибка — json: cannot unmarshal string into Go struct field Profile.id of type int.
Числа в any — float64. 9007199254740993 в map[string]any стало
9.007199254740992e+15, после int64(f) — 9007199254740992, и так же
ушло обратно в JSON. UseNumber сохраняет json.Number("9007199254740993");
поле int64 получает число точно; тег ,string пишет {"id":"9007199254740993"}.
Формы значений. nil-срез — null, пустой — []; []byte("hi") —
"aGk="; ключи карты отсортированы побайтно ("Mid" раньше "alpha",
"10" раньше "9"); <, >, & экранированы как \u003c, \u003e,
\u0026; NaN — json: unsupported value: NaN.
MarshalJSON на указателе вызывается только у адресуемого значения.
json.Marshal(inv) — {"total":1234}, json.Marshal(&inv) —
{"total":"12.34"}; элемент среза метод получает, значение в карте — нет.
Рекурсия. MarshalJSON, зовущий json.Marshal(e), падает:
runtime: goroutine stack exceeds 1048576-byte limit, fatal error: stack overflow, код выхода 2. Цикл в трассе: encoding/json.Marshal →
main.Event.MarshalJSON → … → encoding/json.Marshal. Двойник, объявленный
новым типом (type event Event), рекурсию снимает: методов самого Event у
него нет. Но методы встроенных полей остаются: если встроен time.Time
(раздел 9), двойник реализует json.Marshaler, и json.Marshal(st) даёт
одну строку времени "2026-10-01T12:00:00Z" — id и kind потеряны, ошибки нет.
Двойник-псевдоним (type event = Event) — нет: это тот же тип, и встроенный в
анонимную структуру, он поднимает в неё MarshalJSON — цикл идёт через
(*struct { main.event = main.Event; … }).MarshalJSON.
Цена. json.Marshal структуры из пяти полей — одна аллокация (96 байт),
как и у ручной сборки, и дороже её в 1,8 раза — меньше чем вдвое. Дорогая
сторона — Unmarshal: в записи прогона в 6,0 раза дороже Marshal, 13
аллокаций; в map[string]any — ещё в 1,2 раза дороже, 33 аллокации. В уроке
эти кратности названы округлённо («примерно в шесть раз»): на общей машине
они от запуска к запуску гуляют.
Cents и отрицательные суммы. Формула n/100, n%100 даёт "0.-1" для
−1 и "-1.-23" для −123: остаток в Go несёт знак делимого. Знак и модуль в
uint64 отдельно дают "-0.01" и "-1.23", верно и для math.MinInt64
("-92233720368547758.08").
Потеря точности или ошибка. {"id":9223372036854775808} в поле int64 —
ошибка json: cannot unmarshal number 9223372036854775808 into Go struct field Row.id of type int64, поле осталось 0; в map[string]any — 9.223372036854776e+18
без ошибки.
Строгий разбор (strict.go). {"name":"ann"} {} и {"name":"ann"}x —
400 после второго Decode; тело больше 64 байт — 413 http: request body too large; {"name":"a","name":"b"} — 200 с name="b": v1 дубли ключей
принимает.
encoding/json/v2 (эксперимент go1.25). Повторяющийся ключ v2 отвергает:
jsontext: duplicate object member name "name". Ключ "NAME" не находит поле
name; nil-срез — [], nil-карта — {}; omitempty у int со
значением 0 не пропускает поле; MarshalJSON на указателе вызывается и у
значения без адреса; null обнуляет заполненное поле.
Script
501 lines//go:build ignore
// encoding/json: что пакет решает за вас и о чём не сообщает.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про encoding/json на собеседовании редко спрашивают
// «как вызвать Marshal». Спрашивают про то, что пакет делает МОЛЧА: какие
// поля пропадают, что считается «пустым» для omitempty, почему JSON с
// ключом "NAME" заполнил поле Name, куда делся лишний ключ, почему id из
// базы приехал на одну единицу меньше. Каждый из этих ответов проверяется
// запуском, а не пересказом документации.
//
// ДЕВЯТЬ СЮЖЕТОВ:
//
// 1. Без тегов и с тегами. Одна и та же структура: что выходит в JSON по
// умолчанию и что меняют теги name, omitempty и "-"; неэкспортируемая
// встроенная структура.
// 2. omitempty: что считается пустым. По полю каждого вида — пропущено ли
// оно с omitempty и с omitzero.
// 3. Unmarshal: регистр ключей, лишние ключи, DisallowUnknownFields.
// 4. Unmarshal в заполненную структуру: что остаётся от старого значения,
// включая массив под срезом и элементы-структуры.
// 5. Числа в any: float64 и потеря точности; UseNumber и ",string".
// 6. Формы значений: nil-срез и пустой срез, []byte, порядок ключей карты,
// экранирование HTML, NaN.
// 7. MarshalJSON: получатель-указатель и значение, которое его не находит.
// 8. MarshalJSON без рекурсии: тип-двойник внутри метода. Сама рекурсия
// роняет процесс, поэтому показана отдельно — recursion.sh.
// 9. Двойник не наследует методы самого типа, но методы встроенных полей
// в нём остаются: встроенный time.Time подменяет весь вывод.
//
// ЗАПУСК:
//
// go run bench/gojson/internals.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"math"
"runtime"
"strings"
"time"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func marshal(v any) string {
b, err := json.Marshal(v)
if err != nil {
return "ошибка: " + err.Error()
}
return string(b)
}
func errText(err error) string {
if err == nil {
return "<nil>"
}
return err.Error()
}
// ------------------------------------------------- 1. без тегов и с тегами
type Address struct {
City string `json:"city"`
}
// Account — без тегов: имена полей Go и есть ключи JSON.
type Account struct {
ID int
Name string
Email string
Tags []string
Address Address
password string
}
// AccountTagged — та же структура с тегами.
type AccountTagged struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Tags []string `json:"tags,omitempty"`
Address Address `json:"address,omitempty"`
Password string `json:"-"`
}
// inner — неэкспортируемый тип; встроен в Outer.
type inner struct{ X int }
type Outer struct {
inner
Y int
}
// ------------------------------------------------- 2. что считается пустым
type empty struct{}
// ------------------------------------------------------------ 3. Unmarshal
type Login struct {
Name string `json:"name"`
Role string `json:"role"`
}
type Strict struct {
Name string `json:"name"`
}
// ------------------------------------- 4. Unmarshal в заполненную структуру
type It struct {
A int `json:"a"`
B int `json:"b"`
}
type Box struct {
Items []It `json:"items"`
Tags []string `json:"tags"`
}
type Profile struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Tags []string `json:"tags"`
Meta map[string]int `json:"meta"`
}
// ------------------------------------------------------------- 5. числа
type Row struct {
ID int64 `json:"id"`
}
type RowString struct {
ID int64 `json:"id,string"`
}
// ------------------------------------------------------ 7. MarshalJSON
// Cents объявляет MarshalJSON на УКАЗАТЕЛЕ.
type Cents int64
// Знак и модуль форматируются отдельно. Модуль берётся в uint64: -u для
// uint64 не переполняется, поэтому формула верна и для math.MinInt64.
// Наивная формула naiveCents (раздел 10) на отрицательных даёт "-1.-23".
func (c *Cents) MarshalJSON() ([]byte, error) {
n := int64(*c)
sign, u := "", uint64(n)
if n < 0 {
sign, u = "-", -u
}
return []byte(fmt.Sprintf(`"%s%d.%02d"`, sign, u/100, u%100)), nil
}
// naiveCents — первая версия формулы: деление и остаток со знаком.
func naiveCents(n int64) string {
return fmt.Sprintf(`"%d.%02d"`, n/100, n%100)
}
type Invoice struct {
Total Cents `json:"total"`
}
// --------------------------------------- 8. MarshalJSON без рекурсии
type Event struct {
ID int `json:"id"`
At time.Time `json:"at"`
Kind string `json:"-"`
}
// MarshalJSON добавляет поле "kind" и печатает время в секундах.
//
// Внутри нельзя звать json.Marshal(e): Marshal найдёт у e тот же метод и
// вызовет его снова — бесконечная рекурсия (см. recursion.sh). Тип-двойник
// event объявлен через `type event Event`: поля у него те же, а методов
// самого Event нет (методы встроенных полей остались бы — см. раздел 9),
// и json.Marshal кодирует его по полям.
func (e Event) MarshalJSON() ([]byte, error) {
type event Event
return json.Marshal(struct {
event
At int64 `json:"at"`
Kind string `json:"kind"`
}{event(e), e.At.Unix(), "event:" + e.Kind})
}
// ------------------------ 9. двойник и встроенное поле со своим MarshalJSON
// Stamp встраивает time.Time, у которого есть свой MarshalJSON.
type Stamp struct {
time.Time
ID int `json:"id"`
Kind string `json:"-"`
}
// MarshalJSON устроен так же, как у Event: двойник stamp и поле kind. Но
// новый тип не наследует только методы самого Stamp; методы встроенного
// time.Time остаются в его наборе и поднимаются в анонимную структуру.
func (s Stamp) MarshalJSON() ([]byte, error) {
type stamp Stamp
return json.Marshal(struct {
stamp
Kind string `json:"kind"`
}{stamp(s), s.Kind})
}
// stampTwin — тот же двойник на уровне пакета, чтобы проверить его набор
// методов.
type stampTwin Stamp
func main() {
fmt.Printf("%s %s/%s | encoding/json: теги, omitempty и то, что пакет делает молча\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
// ------------------------------------------------------------------ 1
show("1) ОДНА СТРУКТУРА БЕЗ ТЕГОВ И С ТЕГАМИ")
a := Account{ID: 7, Name: "Ann", password: "secret"}
at := AccountTagged{ID: 7, Name: "Ann", Password: "secret"}
fmt.Println(" без тегов:")
fmt.Println(" ", marshal(a))
fmt.Println(" с тегами name, omitempty и \"-\":")
fmt.Println(" ", marshal(at))
fmt.Println(" password (неэкспортируемое) и Password (тег \"-\") в JSON нет;")
fmt.Println(" пустые Email и Tags с omitempty пропущены, а пустой")
fmt.Println(" Address с omitempty — остался.")
fmt.Println()
at = AccountTagged{ID: 7, Name: "Ann", Email: "ann@example.com",
Tags: []string{"a", "b"}, Password: "secret"}
backIn := `{"ID":8,"NAME":"Bob","role":"admin","password":"hack","tags":["z"]}`
fmt.Println(" обратно: Unmarshal в заполненную AccountTagged")
fmt.Printf(" было: %+v\n", at)
fmt.Printf(" вход: %s\n", backIn)
err := json.Unmarshal([]byte(backIn), &at)
fmt.Printf(" стало: %+v\n", at)
fmt.Printf(" err=%s\n", errText(err))
fmt.Println(" \"ID\" и \"NAME\" нашли поля с тегами id и name; role и password")
fmt.Println(" пропущены молча; Email, которого нет во входе, остался прежним.")
fmt.Println()
fmt.Println(" неэкспортируемое встроенное поле:")
fmt.Printf(" %-34s -> %s\n", "Outer{inner{X: 1}, 2}", marshal(Outer{inner{X: 1}, 2}))
fmt.Println(" Сам inner не виден, но его экспортируемое X поднято наверх.")
// ------------------------------------------------------------------ 2
show("2) OMITEMPTY И OMITZERO: ЧТО СЧИТАЕТСЯ ПУСТЫМ")
zeroTime := time.Time{}
zeroInt := 0
type row struct {
name string
empty any // структура с одним полем omitempty
zero any // структура с одним полем omitzero
}
rows := []row{
{"false", struct {
V bool `json:"v,omitempty"`
}{}, struct {
V bool `json:"v,omitzero"`
}{}},
{"0", struct {
V int `json:"v,omitempty"`
}{}, struct {
V int `json:"v,omitzero"`
}{}},
{`""`, struct {
V string `json:"v,omitempty"`
}{}, struct {
V string `json:"v,omitzero"`
}{}},
{"(*int)(nil)", struct {
V *int `json:"v,omitempty"`
}{}, struct {
V *int `json:"v,omitzero"`
}{}},
{"&zero (указатель на 0)", struct {
V *int `json:"v,omitempty"`
}{&zeroInt}, struct {
V *int `json:"v,omitzero"`
}{&zeroInt}},
{"any(nil)", struct {
V any `json:"v,omitempty"`
}{}, struct {
V any `json:"v,omitzero"`
}{}},
{"[]int(nil)", struct {
V []int `json:"v,omitempty"`
}{}, struct {
V []int `json:"v,omitzero"`
}{}},
{"[]int{}", struct {
V []int `json:"v,omitempty"`
}{[]int{}}, struct {
V []int `json:"v,omitzero"`
}{[]int{}}},
{"map[string]int{}", struct {
V map[string]int `json:"v,omitempty"`
}{map[string]int{}}, struct {
V map[string]int `json:"v,omitzero"`
}{map[string]int{}}},
{"[2]int{}", struct {
V [2]int `json:"v,omitempty"`
}{}, struct {
V [2]int `json:"v,omitzero"`
}{}},
{"struct{}{}", struct {
V empty `json:"v,omitempty"`
}{}, struct {
V empty `json:"v,omitzero"`
}{}},
{"Address{}", struct {
V Address `json:"v,omitempty"`
}{}, struct {
V Address `json:"v,omitzero"`
}{}},
{"time.Time{}", struct {
V time.Time `json:"v,omitempty"`
}{zeroTime}, struct {
V time.Time `json:"v,omitzero"`
}{zeroTime}},
}
fmt.Printf(" %-24s %-36s %s\n", "значение поля", "с omitempty", "с omitzero")
for _, r := range rows {
fmt.Printf(" %-24s %-36s %s\n", r.name, marshal(r.empty), marshal(r.zero))
}
fmt.Println(" {} — поле пропущено. omitempty смотрит на длину и нулевое")
fmt.Println(" значение у чисел, bool, указателей и интерфейсов; структура")
fmt.Println(" для него никогда не пуста. omitzero спрашивает IsZero() или")
fmt.Println(" нулевое значение типа: пустой, но не nil срез для него не ноль.")
// ------------------------------------------------------------------ 3
show("3) UNMARSHAL: РЕГИСТР КЛЮЧЕЙ И ЛИШНИЕ КЛЮЧИ")
for _, in := range []string{
`{"name":"ann"}`,
`{"Name":"ann"}`,
`{"NAME":"ann"}`,
`{"nAmE":"ann"}`,
} {
var l Login
err := json.Unmarshal([]byte(in), &l)
fmt.Printf(" %-34s -> Name=%q err=%s\n", in, l.Name, errText(err))
}
in := `{"name":"ann","role":"user","is_admin":true}`
var s Strict
err = json.Unmarshal([]byte(in), &s)
fmt.Printf(" Unmarshal в Strict{Name}:\n %s\n -> Name=%q err=%s\n", in, s.Name, errText(err))
var s2 Strict
dec := json.NewDecoder(strings.NewReader(in))
dec.DisallowUnknownFields()
err = dec.Decode(&s2)
fmt.Printf(" Decoder с DisallowUnknownFields, тот же вход:\n -> Name=%q err=%s\n", s2.Name, errText(err))
// ------------------------------------------------------------------ 4
show("4) UNMARSHAL В ЗАПОЛНЕННУЮ СТРУКТУРУ")
p := Profile{
ID: 1, Name: "Ann", Email: "ann@example.com",
Tags: []string{"a", "b", "c"},
Meta: map[string]int{"visits": 3},
}
fmt.Printf(" было: %+v\n", p)
upd := `{"name":"Bob","tags":["z"],"meta":{"likes":5},"email":null}`
fmt.Printf(" вход: %s\n", upd)
err = json.Unmarshal([]byte(upd), &p)
fmt.Printf(" стало: %+v\n", p)
fmt.Printf(" err=%s\n", errText(err))
fmt.Println(" id нет во входе — осталось 1; email: null для string — ничего")
fmt.Println(" не изменил; срез обрезан до нуля и заполнен заново, карта дополнена.")
fmt.Println()
old := []string{"a", "b", "c"}
bx := Box{Items: []It{{1, 2}, {3, 4}}, Tags: old}
boxIn := `{"items":[{"a":5}],"tags":["z"]}`
fmt.Printf(" было: %+v\n", bx)
fmt.Printf(" вход: %s\n", boxIn)
err = json.Unmarshal([]byte(boxIn), &bx)
fmt.Printf(" стало: %+v\n", bx)
fmt.Printf(" err=%s\n", errText(err))
fmt.Printf(" старый срез old после разбора: %v\n", old)
fmt.Println(" Срез обрезан до нуля и дописан в тот же массив: old видит z.")
fmt.Println(" Элемент-структура слит со старым: B=2 остался от {1 2}.")
fmt.Println()
var bad Profile
err = json.Unmarshal([]byte(`{"id":"7","name":"Bob"}`), &bad)
fmt.Printf(" тип не тот: {\"id\":\"7\",\"name\":\"Bob\"}\n -> ID=%d Name=%q\n err=%s\n", bad.ID, bad.Name, errText(err))
// ------------------------------------------------------------------ 5
show("5) ЧИСЛА В any: FLOAT64 И ПОТЕРЯ ТОЧНОСТИ")
big := `{"id":9007199254740993}`
var m map[string]any
_ = json.Unmarshal([]byte(big), &m)
f := m["id"]
fmt.Printf(" вход %s\n", big)
fmt.Printf(" в map[string]any: %v (%T)\n", f, f)
fmt.Printf(" int64(f) %d\n", int64(f.(float64)))
back, _ := json.Marshal(m)
fmt.Printf(" json.Marshal обратно %s\n", back)
var mn map[string]any
d := json.NewDecoder(strings.NewReader(big))
d.UseNumber()
_ = d.Decode(&mn)
n := mn["id"].(json.Number)
ni, _ := n.Int64()
fmt.Printf(" с UseNumber: %v (%T)\n", mn["id"], mn["id"])
fmt.Printf(" n.Int64() %d\n", ni)
var r Row
_ = json.Unmarshal([]byte(big), &r)
fmt.Printf(" в struct{ ID int64 }: %d\n", r.ID)
fmt.Printf(" 2^53 = %d\n", int64(1)<<53)
fmt.Printf(" с тегом \",string\": %s\n", marshal(RowString{ID: 9007199254740993}))
var rs RowString
err = json.Unmarshal([]byte(`{"id":"9007199254740993"}`), &rs)
fmt.Printf(" и обратно: %d err=%s\n", rs.ID, errText(err))
err = json.Unmarshal([]byte(big), &rs)
fmt.Printf(" \",string\", а пришло число: err=%s\n", errText(err))
// ------------------------------------------------------------------ 6
show("6) ФОРМЫ ЗНАЧЕНИЙ")
fmt.Printf(" %-30s -> %s\n", "[]string(nil)", marshal([]string(nil)))
fmt.Printf(" %-30s -> %s\n", "[]string{}", marshal([]string{}))
fmt.Printf(" %-30s -> %s\n", "map[string]int(nil)", marshal(map[string]int(nil)))
fmt.Printf(" %-30s -> %s\n", `[]byte("hi")`, marshal([]byte("hi")))
fmt.Printf(" %-30s -> %s\n", "[]uint8{1, 2, 3}", marshal([]uint8{1, 2, 3}))
keys := map[string]int{"zeta": 1, "alpha": 2, "Mid": 3, "beta": 4}
fmt.Printf(" %-30s -> %s\n", "map{zeta alpha Mid beta}", marshal(keys))
fmt.Printf(" %-30s -> %s\n", `map[int]string{10:"a", 9:"b"}`, marshal(map[int]string{10: "a", 9: "b"}))
fmt.Printf(" %-30s -> %s\n", `"<b>Tom & Jerry</b>"`, marshal("<b>Tom & Jerry</b>"))
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
_ = enc.Encode("<b>Tom & Jerry</b>")
fmt.Printf(" %-30s -> %s\n", "Encoder, SetEscapeHTML(false)", strings.TrimSpace(buf.String()))
fmt.Printf(" %-30s -> %s\n", "math.NaN()", marshal(math.NaN()))
fmt.Printf(" %-30s -> %s\n", "math.Inf(1)", marshal(math.Inf(1)))
fmt.Printf(" %-30s -> %s\n", "struct{ V float64 }{NaN}", marshal(struct{ V float64 }{math.NaN()}))
fmt.Printf(" %-30s -> %s\n", "make(chan int)", marshal(make(chan int)))
// ------------------------------------------------------------------ 7
show("7) MARSHALJSON НА УКАЗАТЕЛЕ: КОГДА ОН НЕ ВЫЗЫВАЕТСЯ")
c := Cents(1234)
inv := Invoice{Total: 1234}
fmt.Printf(" %-30s -> %s\n", "json.Marshal(c)", marshal(c))
fmt.Printf(" %-30s -> %s\n", "json.Marshal(&c)", marshal(&c))
fmt.Printf(" %-30s -> %s\n", "json.Marshal(inv)", marshal(inv))
fmt.Printf(" %-30s -> %s\n", "json.Marshal(&inv)", marshal(&inv))
fmt.Printf(" %-30s -> %s\n", "[]Invoice{inv}", marshal([]Invoice{inv}))
fmt.Printf(" %-30s -> %s\n", "map[string]Invoice{\"a\": inv}", marshal(map[string]Invoice{"a": inv}))
fmt.Println(" Метод объявлен на *Cents. Значение, у которого нет адреса,")
fmt.Println(" метода не имеет — и кодируется как обычное число, без ошибки.")
// ------------------------------------------------------------------ 8
show("8) MARSHALJSON БЕЗ РЕКУРСИИ: ТИП-ДВОЙНИК")
ev := Event{ID: 5, At: time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC), Kind: "login"}
fmt.Printf(" %-24s -> %s\n", "json.Marshal(ev)", marshal(ev))
type plain Event
fmt.Printf(" %-24s -> %s\n", "json.Marshal(plain(ev))", marshal(plain(ev)))
fmt.Println(" Внутри MarshalJSON кодируется event — тип с теми же полями,")
fmt.Println(" но без методов самого Event. json.Marshal(e) на его месте")
fmt.Println(" вызвал бы тот же MarshalJSON снова: см. recursion.sh.")
// ------------------------------------------------------------------ 9
show("9) ДВОЙНИК НЕ СПАСАЕТ ОТ МЕТОДОВ ВСТРОЕННОГО ПОЛЯ")
st := Stamp{Time: time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC), ID: 5, Kind: "login"}
b, err := json.Marshal(st)
fmt.Printf(" json.Marshal(st) -> %s err=%s\n", b, errText(err))
_, ok := any(stampTwin{}).(json.Marshaler)
fmt.Printf(" type stampTwin Stamp реализует json.Marshaler: %v\n", ok)
fmt.Println(" Stamp встраивает time.Time. Двойник не наследует методы Stamp,")
fmt.Println(" но MarshalJSON встроенного time.Time в нём остался и поднялся")
fmt.Println(" в анонимную структуру: вышла одна строка времени, id и kind")
fmt.Println(" потеряны, ошибки нет.")
// ----------------------------------------------------------------- 10
show("10) CENTS: НАИВНАЯ ФОРМУЛА И ОТРИЦАТЕЛЬНЫЕ СУММЫ")
fmt.Printf(" %-22s %-26s %s\n", "значение", "n/100, n%100", "знак и модуль отдельно")
for _, n := range []int64{0, 1, 99, 100, -1, -100, -123, math.MaxInt64, math.MinInt64} {
c := Cents(n)
fmt.Printf(" %-22d %-26s %s\n", n, naiveCents(n), marshal(&c))
}
fmt.Println(" Остаток от деления в Go несёт знак делимого: -123 % 100 = -23,")
fmt.Println(" и наивная формула печатает минус дважды. JSON при этом верный —")
fmt.Println(" неверна сумма внутри строки.")
// ----------------------------------------------------------------- 11
show("11) ПОТЕРЯ ТОЧНОСТИ ИЛИ ОШИБКА: ЧИСЛО ВНЕ int64")
huge := `{"id":9223372036854775808}`
var rh Row
err = json.Unmarshal([]byte(huge), &rh)
fmt.Printf(" вход: %s (2^63, на единицу больше MaxInt64)\n", huge)
fmt.Printf(" в struct{ ID int64 }: ID=%d\n err=%s\n", rh.ID, errText(err))
var mh map[string]any
err = json.Unmarshal([]byte(huge), &mh)
fmt.Printf(" в map[string]any: %v err=%s\n", mh["id"], errText(err))
fmt.Println(" В поле int64 число не помещается — ошибка UnmarshalTypeError.")
fmt.Println(" В any оно становится float64: без ошибки, точность может уйти.")
}