Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Go 1.27: generic-методы, которых не видит reflect

Дата публикации: 19-08-2026 17:29:27

Go 1.27 (на момент написания rc3, финал обещают в августе) приносит generic-методы: метод наконец может объявить собственный тип-параметр. Взял golang:1.27rc3 в Docker и проверил, где у фичи границы. reflect новый метод не видит вообще, хотя go/types на том же типе показывает его в method set — метод доступен компилятору и недоступен через рефлексию, поэтому ни один интерфейс им не удовлетворить, а валидаторы и ORM его не заметят. Померил цену: на 200 инстанцирований с запретом инлайна бинарь с методом больше на 2264 байта, из которых 2000 приходятся на длину имён символов, а не на код; по времени разницы нет — это generic-функция с ресивером и тот же gcshape-стенсилинг. А вот что реально заметит прод на апгрейде: GODEBUG=asynctimerchan=1 теперь роняет процесс до main, encoding/json v1 на движке v2 меняет байты вывода на битом UTF-8, json.RawMessage обзавёлся String() и поменял ваши логи. Плюс замеры нового аллокатора: минус четверть времени на new(16 B) и ноль на 128 байтах. Читать дальше →

Основное содержимое страницы с новостью.

Время на прочтение4 мин

Охват и читатели10K

У Box[int] два метода. reflect видит один.

type Box[T any] struct{ v T }

func (b Box[T]) Value() T { return b.v }

func (b Box[T]) Map[U any](f func(T) U) Box[U] {
	return Box[U]{v: f(b.v)}
}
go: go1.27rc3
chain: value=42
reflect NumMethod: 1
  method: Value func(main.Box[int]) int

Метод с собственным тип-параметром, независимым от ресивера, — главное языковое изменение Go 1.27 (пока rc3, ноты в статусе draft, финал обещают в августе). Box[int] теперь превращается в Box[string] прямо в цепочке вызовов, без package-level хелпера. Англоязычные обзоры успели похоронить под этим старый Go; я собрал контейнер с golang:1.27rc3 и проверил фичу за пределами примера из нот.

Метод есть, но не для всех

Пропажа выборочная. Тот же тип глазами компилятора, через go/types:

go/types NewMethodSet len: 2
   Map func[U any](f func(int) U) ex/v1/src.Box[U]
   Value func() int

Два метода. В method set языка Map есть, а reflect его не находит ни у Box[int], ни у *Box[int]. Код метода исполняется как обычно, недоступен именно рантайм-доступ: API рефлексии не умеет представить generic-значение и инстанцировать его на лету.

Отсюда потолок фичи. Интерфейс с ровно той сигнатурой, которую даёт U = string, не удовлетворяется:

cannot use Box[int]{…} (value of struct type Box[int]) as StringMapper value in variable
declaration: Box[int] does not implement StringMapper (wrong type for method Map)
                have Map[U any](func(int) U) Box[U]
                want Map(func(int) string) Box[string]

Объявить такой метод внутри интерфейса тоже нельзя: interface method must have no type parameters.

Всё, что ходит по типам рефлексией в рантайме, новых методов не увидит: валидаторы структур, ORM, DI-контейнеры. Инструменты на go/types — линтеры, часть генераторов — увидят спокойно, так что поддержка у разных тулов появится в разное время.

Method value без инстанцирования тоже не соберётся, и компилятор в диагностике называет метод функцией:

./main.go:11:7: cannot use generic function b.Map without instantiation
./main.go:13:7: cannot use generic function Box[int].Map without instantiation

А b.Map[string] и method expression Box[int].Map[string] собираются нормально. Присваивание var fn func(func(int) string) Box[string] = b.Map тоже проходит, но это не заслуга релиза: вывод типов при присваивании работает с 1.21. В 1.27 его расширили на конверсии и композитные литералы — слайс []func([]int) int{first, last} из generic-функций на 1.26 падает с cannot use generic function first without instantiation, на 1.27rc3 собирается молча.

Две тысячи байт на имена

Раз компилятор говорит «функция», интересно, во сколько обходится новый синтаксис. Первый замер я запорол: двести типов одной формы, вызовы заинлайнились и схлопнулись, в бинаре от инстанцирований не осталось ничего. Правильный вариант: двести разных шейпов (разный размер, разная разметка указателей), //go:noinline на трансформации, одинаковый module path, отличается ровно одна строчка объявления.

bin_method: 2780409 bytes
bin_func:   2778145 bytes

go tool nm показывает в обоих бинарях все 400 символов: 200 стенсилов и 200 словарей. Разница 2264 байта, и код тут почти ни при чём — суммарная длина имён main-символов у метода 18402 байта против 16402 у функции. Две тысячи из 2264 это буквально имена: main..dict.Box[int].Map[main.T100] длиннее, чем main..dict.Map[int,main.T100].

По времени разницы нет. С запретом инлайна медиана семи прогонов даёт 13.30 ns/op у метода против 13.75 у функции; если инлайн разрешить, обе версии съедаются целиком, и -gcflags=-m показывает почему:

can inline Box[go.shape.int].MapM[go.shape.string]
can inline MapF[go.shape.int,go.shape.string]

Тот же gcshape-стенсилинг: generic-метод компилируется как generic-функция с ресивером. За две квадратные скобки вы платите длиной имён, а получаете неймспейсинг и цепочку, которая читается слева направо.

Заодно померил обещанное бесплатное ускорение: компилятор зовёт size-специализированные функции аллокации, в нотах заявлено до 30% на объектах меньше 80 байт. Медиана девяти прогонов на Ryzen 7 PRO 8700GE:

Размер

1.26

1.27rc3

new(16 B)

13.98 ns

10.68 ns

new(80 B)

17.33 ns

14.25 ns

new(128 B)

19.00 ns

19.46 ns

На 16 байтах минус четверть времени, на 128 разницы нет. Заявленные 60 КБ прибавки к бинарю тоже не сошлись: hello-world отличается на 40 КБ от сборки с GOEXPERIMENT=nosizespecializedmalloc, а против 1.26 тот же бинарь стал на 55 КБ меньше.

Что действительно заметит прод

Апгрейд на 1.27 больнее тронет места, про которые в обзорах строчка мелким шрифтом.

Каналы таймеров стали синхронными ещё в 1.23, а GODEBUG=asynctimerchan=1 возвращал старое буферизованное поведение. В 1.27 переключатель удалён по регламенту GODEBUG, и удалён жёстко: процесс падает до первой строчки main, без варнинга в лог.

fatal error: removed GODEBUG "asynctimerchan" set to old value "1" in environment

Если эта переменная досталась вам по наследству в Dockerfile, systemd-юните или values.yaml, вы это увидите на выкатке.

Дальше encoding/json: пакет v1 теперь работает поверх движка v2, и обещано, что поведение сохранено. Семантика сохранена, байты нет:

1.26:  {"k":"A\ufffdB"}   7b 22 6b 22 3a 22 41 5c 75 66 66 66 64 42 22 7d
1.27:  {"k":"A�B"}        7b 22 6b 22 3a 22 41 ef bf bd 42 22 7d

Раньше невалидная последовательность экранировалась в \ufffd, теперь уходят сырые байты U+FFFD. То же с ключами map. Распарсится одинаково, а длина тела, его хеш и подпись меняются: golden-тесты, подписанные вебхуки, побайтовое сравнение ответов — если в данные попадает пользовательский мусор, это ваш случай. Аварийный выход — GOEXPERIMENT=nojsonv2. Оттуда же мелочь, которая тихо поменяет логи: json.RawMessage обзавёлся методом String(), %v теперь печатает {"a":1} вместо [123 34 97 34 58 49 125], а nil — null вместо [].

Перед апгрейдом я бы грепнул инфраструктуру на asynctimerchan, а тесты на побайтовые сравнения JSON. Generic-методы в этом списке последние: они добавляют возможность, поведение уже написанного кода апгрейд оставляет как было.

Тулчейн ставить не надо:

docker run --rm -v "$PWD":/w -w /w -u "$(id -u):$(id -g)" \
  -e HOME=/w -e GOCACHE=/w/.cache golang:1.27rc3 go test -bench . -benchmem

Замена образа на golang:1.26 даёт вторую половину сравнения.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Go 1.27 подменил движок encoding/json. Замерил три конфигурации и нашёл, где стало хуже01003-09-2026
2Утёкшие горутины теперь ищет сборщик мусора. Проверил, где он молчит08.3331-08-2026
3[Перевод] Ветвление оказалось дороже лишней работы: как GitHub разогнал обработку исходного кода до 45 ГиБ/с010.3621-08-2026
4Написание ядра ОС с нуля. Часть 3 — Пропуск в высшую лигу08.6623-08-2026
5[Перевод] Rust 1.98.0: алгебраические методы для f32,f64, исправление взаимодействия между ManuallyDrop и Box011.2131-08-2026
6Go SDK для YDB: уменьшаем количество запросов к СУБД для интерактивных транзакций08.813-08-2026
7Как на ровном месте сэкономить 1000+ ядер, или Куда на самом деле уходили 80% CPU09.1208-09-2026
8Моя собственная Gated RNN: как работает? (и бенчмарки, конечно же)05.6226-09-2026
9Миллион контейнеров и быстрый откат релиза: эволюция деплоя в RTC07.4918-08-2026

Классификация: Пресс-релизы. Схожих патентов: 0. Схожих новостей: 9. Тональность: 0. Информативность: 13.12. Источник: habr.com.