Deep Engineering

ЗАМЕР

bench/gojson/internals.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/json
Как запустить
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

Запись прогона

Замеры для урока «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 обнуляет заполненное поле.

Скрипт

501 строк
//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: без ошибки, точность может уйти.")
}