Go Decoder.UseNumber: как не округлить большой ID в map[string]any

API получил идентификатор 9007199254740993, а после обработки вернул другое число. При декодировании в interface{} JSON-числа могут стать float64, который не представляет все большие целые точно. Для идентификатора такая потеря меняет сам объект, а не только формат отображения.

Как разобрать ситуацию

Для известной структуры можно декодировать прямо в подходящий целочисленный тип с проверкой диапазона. Проблемный случай этой статьи — динамическая map[string]any: обычный decoder использует float64 для числа. Decoder.UseNumber сохраняет его как json.Number. После этого всё равно нужно проверить диапазон и формат перед арифметикой или обращением к базе.

Пример и ожидаемый результат

Минимальная программа дважды читает объект с числом 9007199254740993 в map[string]any: обычным Unmarshal и Decoder с UseNumber. Первый путь теряет последнюю единицу, второй сохраняет точный числовой текст. Тест использует число за границей целых, безопасно представимых float64.

package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

func main() {
	raw := `{"id":9007199254740993}`
	var plain map[string]any
	if err := json.Unmarshal([]byte(raw), &plain); err != nil {
		panic(err)
	}
	fmt.Printf("%.0f\n", plain["id"].(float64))
	var exact map[string]any
	d := json.NewDecoder(strings.NewReader(raw))
	d.UseNumber()
	if err := d.Decode(&exact); err != nil {
		panic(err)
	}
	fmt.Println(exact["id"].(json.Number).String())
}

Ожидаемый вывод — сначала 9007199254740992, затем исходное 9007199254740993. Это конкретное различие двух режимов декодирования Go, проверяемое локально стандартной библиотекой.

Проверка перед применением

  • Сравните ввод и вывод как точное целое или строку, а не с округлением.
  • Проверьте максимально допустимый ID и значение за его пределом.
  • Согласуйте формат с JavaScript-клиентами: исправление только сервера не устраняет потерю на другой стороне.

Добавьте такой ID в контрактные фикстуры интеграции, даже если текущая база пока выдаёт маленькие значения. Ошибка может проявиться при подключении внешнего поставщика или миграции генератора ключей. Проверка одного поля важнее общего равенства размера JSON: повреждённый идентификатор сохраняет и корректный синтаксис, и привычную длину ответа.

Границы решения

UseNumber не выполняет бизнес-валидацию и не превращает дробные значения в подходящие целые. Если контракт требует int64, проверяйте Int64 и возвращаемую ошибку. Для арифметики денег необходимо отдельное согласованное представление.

Если базовый Go уже знаком и хочется связать HTTP, хранение данных и обработку отказов в одну систему, посмотрите наш платный курс

Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker. Перед записью сравните программу со своей задачей и подготовкой.

Техническая справка: pkg.go.dev: документация.

Следующий шаг: Go backend: HTTP, конкурентность и устойчивость сервиса. По смежным вопросам: Как оборачивать ошибки в Go и не потерять машинную проверку причины, Отмена HTTP-запроса в Go: как убедиться, что context дошёл до клиента.