У 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 даёт вторую половину сравнения.