MEASUREMENT
bench/gojson/jsonv2.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
139 lines//go:build goexperiment.jsonv2
// encoding/json и encoding/json/v2 на одних и тех же входах. ЭКСПЕРИМЕНТ.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. В go1.25 появился пакет encoding/json/v2 — но только
// за флагом GOEXPERIMENT=jsonv2, вне обещания совместимости Go 1. Его
// документация перечисляет, чем v2 отличается от v1, и почти каждое
// отличие — ровно одно из «молчаливых» поведений, о которых урок: регистр
// ключей, nil-срез, omitempty у чисел, метод на указателе, повторяющийся
// ключ. Здесь каждое из
// них прогоняется через оба пакета.
//
// Без флага файл не собирается: build-ограничение goexperiment.jsonv2.
//
// ЗАПУСК (только go1.25+):
//
// GOEXPERIMENT=jsonv2 go run bench/gojson/jsonv2.go
package main
import (
"fmt"
"runtime"
"strings"
jsonv1 "encoding/json"
jsonv2 "encoding/json/v2"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func errText(err error) string {
if err == nil {
return "<nil>"
}
return err.Error()
}
func m1(v any) string {
b, err := jsonv1.Marshal(v)
if err != nil {
return "ошибка: " + err.Error()
}
return string(b)
}
func m2(v any) string {
b, err := jsonv2.Marshal(v)
if err != nil {
return "ошибка: " + err.Error()
}
return string(b)
}
type Login struct {
Name string `json:"name"`
}
type List struct {
Tags []string `json:"tags"`
Meta map[string]int `json:"meta"`
}
type Counter struct {
N int `json:"n,omitempty"`
}
type Cents int64
// Та же формула, что в internals.go: знак и модуль отдельно.
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
}
type Invoice struct {
Total Cents `json:"total"`
}
type Profile struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
fmt.Printf("%s %s/%s | encoding/json (v1) и encoding/json/v2 — ЭКСПЕРИМЕНТ\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
show("1) РЕГИСТР КЛЮЧЕЙ ПРИ UNMARSHAL")
for _, in := range []string{`{"name":"ann"}`, `{"NAME":"ann"}`} {
var a, b Login
e1 := jsonv1.Unmarshal([]byte(in), &a)
e2 := jsonv2.Unmarshal([]byte(in), &b)
fmt.Printf(" %-16s v1: Name=%-6q err=%s\n", in, a.Name, errText(e1))
fmt.Printf(" %-16s v2: Name=%-6q err=%s\n", "", b.Name, errText(e2))
}
show("2) NIL-СРЕЗ И NIL-КАРТА")
fmt.Printf(" List{} v1: %s\n", m1(List{}))
fmt.Printf(" List{} v2: %s\n", m2(List{}))
show("3) OMITEMPTY У ЧИСЛА")
fmt.Printf(" Counter{N: 0} v1: %s\n", m1(Counter{}))
fmt.Printf(" Counter{N: 0} v2: %s\n", m2(Counter{}))
show("4) MARSHALJSON НА УКАЗАТЕЛЕ, ЗНАЧЕНИЕ БЕЗ АДРЕСА")
inv := Invoice{Total: 1234}
fmt.Printf(" json.Marshal(inv) v1: %s\n", m1(inv))
fmt.Printf(" json.Marshal(inv) v2: %s\n", m2(inv))
show("5) NULL В ЗАПОЛНЕННОЕ ПОЛЕ")
in := `{"email":null}`
p1 := Profile{Name: "Ann", Email: "ann@example.com"}
p2 := p1
_ = jsonv1.Unmarshal([]byte(in), &p1)
_ = jsonv2.Unmarshal([]byte(in), &p2)
fmt.Printf(" %s v1: %+v\n", in, p1)
fmt.Printf(" %s v2: %+v\n", in, p2)
show("6) ПОВТОРЯЮЩИЙСЯ КЛЮЧ")
dup := `{"name":"a","name":"b"}`
var d1, d2 Login
e1 := jsonv1.Unmarshal([]byte(dup), &d1)
e2 := jsonv2.Unmarshal([]byte(dup), &d2)
fmt.Printf(" %s v1: Name=%q err=%s\n", dup, d1.Name, errText(e1))
fmt.Printf(" %s v2: Name=%q err=%s\n", dup, d2.Name, errText(e2))
fmt.Println()
fmt.Println(" encoding/json/v2 существует только с GOEXPERIMENT=jsonv2 и не")
fmt.Println(" подпадает под обещание совместимости Go 1.")
}