Go 1.27, или Как я перестал беспокоиться и полюбил Generic Methods

Релиз Go 1.27 состоится в августе 2026 года, и на этот раз сам язык претерпел изменения. Это не просто «мы добавили новую функцию в slices» — это реальные изменения. Появились обобщенные методы (generic methods). Закрыта проблема одиннадцатилетней давности. Пакет encoding/json был незаметно заменен на новый движок прямо во время работы системы.
Все приведенные ниже примеры были выполнены на go1.27rc3 (darwin/arm64). Каждый блок вывода является фактическим результатом, скопированным напрямую, включая сообщения об ошибках. Если что-то выглядит странно, значит, именно так ответил компилятор.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Изменения в языке (да, действительно)
1.1. Обобщенные методы
Начиная с Go 1.18 у нас есть обобщения (generics), и с того же момента ведутся дискуссии на эту тему. Они выглядят примерно так:
«Позвольте мне просто написать метод
Mapдля моего типа среза (slice)—»
method must have no type parameters«...хорошо. Будет функцией на уровне пакета».
Методы могли использовать только параметры типа, объявленные получателем (receiver). Ваш собственный метод не мог вводить новые параметры. В результате каждое обобщенное преобразование отправлялось в область видимости пакета, где соседствовало с семнадцатью другими вспомогательными функциями с названиями вроде MapSlice.
Go 1.27 исправляет это:
1type List[E any] []E
2
3// F — это параметр типа, объявленный самим методом
4func (l List[E]) Apply[F any](f func(E) F) List[F] {
5 r := make(List[F], len(l))
6 for i, x := range l {
7 r[i] = f(x)
8 }
9 return r
10}
11
12func main() {
13 l := List[int]{1, 2, 3}
14 fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
15 // [10 20 30]
16}
Получателю даже не обязательно быть обобщенным. Обычная структура может иметь обобщенный метод:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Прежде чем вы начнете переписывать всю кодовую базу сегодня днем, учтите два ограничения, которые важнее, чем кажутся на первый взгляд:
- Методы интерфейсов не могут объявлять параметры типа.
- Обобщенные методы не могут реализовывать методы интерфейсов.
Именно второе ограничение доставит вам проблем:
1type Adder interface{ Add(int) int }
2
3type G struct{}
4
5func (G) Add[T any](v T) T { return v }
6
7var _ Adder = G{}
1cannot use G{} (value of struct type G) as Adder value in variable declaration:
2 G does not implement Adder (wrong type for method Add)
3 have Add[T any](T) T
4 want Add(int) int
Таким образом, обобщенные методы и динамическая диспетчеризация несовместимы. Команда Go не делает это из жадности — дело в том, что обобщенный метод имеет бесконечное количество реализаций (instantiations), а таблица методов интерфейса должна быть конечной и известной на этапе компиляции. Вы не можете поместить бесконечное множество в конечную таблицу. Законы программирования сказали «нет».
Практический вывод: обобщенные методы предназначены для конкретных типов с API утилитарного характера. Все, что должно скрываться за интерфейсом, по-прежнему требует старого подхода.
Стандартная библиотека уже воспользовалась этим преимуществом. У Rand из math/rand/v2 появился обобщенный метод:
1func (r *Rand) N[Int intType](n Int) Int
1r := rand.New(rand.NewPCG(1, 2))
2fmt.Println(r.N(int32(100))) // 76
3fmt.Println(r.N(time.Second)) // 616.436223ms
Ранее обобщенной была только функция rand.N на уровне пакета, что означало использование глобального источника. Теперь ваш собственный инициализированный (seeded) *Rand обладает такой же удобной функциональностью. Мелочь, а приятно.
1.2. Селекторы полей в литералах структур, или: проблема №9859 наконец решена
Встраивание (embedding) дает вам u.ID. Но встраивание не дает вам User{ID: 1}. Вместо этого оно дает User{Base: Base{ID: 1}}, что является способом Go спросить, действительно ли вы этого хотели.
Проблема №9859 была открыта в 2015 году. Теперь она закрыта. Где-то гофер, который с тех пор дважды сменил профессию, получает уведомление на GitHub.
1type Object struct{ name, color string }
2type Point3D struct {
3 Object
4 x, y, z float64
5}
6type Line struct {
7 Object
8 p, q Point3D
9}
10
11// Go 1.27: используйте продвигаемое поле напрямую
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name не является полем Line. Это поле встроенного Object. Раньше вам пришлось бы писать Line{Object: Object{name: "diagonal"}, ...}.
Теперь о мелком шрифте, который я проверил, пытаясь скомпилировать все это.
(а) Ключом по-прежнему является простой идентификатор. Вы не можете написать произвольный путь селектора.
1_ = Line{Object.name: "x"} // недопустимое имя поля Object.name в литерале структуры
2_ = Line{p.x: 1} // недопустимое имя поля p.x в литерале структуры
Так что функция звучит не как «ключи теперь работают как доступ к полям». Она специфична: «неявно продвигаемые имена полей разрешены». Чуть уже, чем предполагает заголовок.
(б) Вы не можете одновременно указать встроенное поле и что-то, продвинутое из него.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// невозможно одновременно указать продвигаемое имя поля и охватывающее встроенное поле Object
Разумно. Вы дали две противоречивые инструкции относительно одной и той же области памяти, и компилятор отказался угадывать.
(в) Встраивание указателей не поддерживается.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// недопустимое неявное разыменование указателя для доступа к name
Чтобы пройти по указателю, нужен указатель, а во время создания литерала его еще нет. Это тоже справедливо.
Хорошая новость: go fix перепишет ваши старые литералы за вас. Подробнее об этом позже.
1.3. Вывод типов функций стал менее произвольным
Вывод типов для обобщенных функций раньше работал в одних местах и не работал в других, без видимых принципов. Присваивание переменной? Работает. Помещение той же функции в поле структуры? Ошибка компилятора, пожалуйста, пишите double[int], как в 2022 году.
Go 1.27 заставляет вывод типов работать в любом контексте, где целевой тип однозначно известен:
1func double[T ~int | ~float64](v T) T { return v * 2 }
2
3type S struct{ f func(int) int }
4type A [1]func(int) int
5
6func main() {
7 s := S{f: double} // 1.26: требовалось double[int]
8 a := A{double} // 1.26: требовалось double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: требовалось double[int]
12
13 var fn func(float64) float64 = double // это работало всегда
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Одна функция double, инстанцированная как func(int) int в трех местах и как func(float64) float64 в четвертом, полностью исходя из контекста. Если вы пишете структуры конфигурации или таблицы обработчиков, полные функциональных значений, куча шума от [T] скоро исчезнет из вашей кодовой базы.
2. Среда выполнения: бесплатная производительность и детектор утечек
2.1. Выделение памяти с учетом размера (Size-Specialized Allocation)
Компилятор теперь генерирует вызовы к подпрограммам выделения памяти, оптимизированным под размер, для небольших объектов. В примечаниях к выпуску говорится о снижении количества выделений до 30% для объектов менее 80 байт и около 1% в целом для программ с интенсивным выделением памяти.
Я не верю примечаниям к выпуску без измерений, поэтому:
1type small struct{ a, b, c, d int }
2
3var sink any
4
5func BenchmarkAlloc(b *testing.B) {
6 for b.Loop() {
7 sink = &small{}
8 }
9}
10
11func BenchmarkSlice(b *testing.B) {
12 for b.Loop() {
13 sink = make([]byte, 48)
14 }
15}
1# по умолчанию (size-specialized malloc включен)
2BenchmarkAlloc-12 3000000 5.358 нс/оп
3BenchmarkSlice-12 3000000 13.58 нс/оп
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12 3000000 8.722 нс/оп
7BenchmarkSlice-12 3000000 20.81 нс/оп
На 30–35% быстрее в микротесте, который только и делает, что выделяет память на Apple Silicon. Иными словами: это лучший случай, достигнутый в лабораторных условиях тестом, разработанным для улучшения показателей. В реальной программе сборка мусора и полезная нагрузка нивелируют большую часть этого прироста. Заявление о ~1% — самое честное.
Цена — размер бинарного файла. Hello World:
12413202 байт (по умолчанию)
22361826 байт (nosizespecializedmalloc)
Около 50 КБ, фиксированно, независимо от программы. Вы можете отказаться от этого с помощью GOEXPERIMENT=nosizespecializedmalloc, но эта лазейка запланирована к удалению в Go 1.28, поэтому рассматривайте ее как обходной путь для отчетов об ошибках, а не как постоянное решение.
2.2. Профиль утечек горутин теперь реален
Экспериментальная функция в Go 1.26, общедоступная в 1.27. GOEXPERIMENT для goroutineleakprofile больше не нужен; это просто работает.
1func leak() {
2 ch := make(chan int) // никто никогда не отправит. никто.
3 go func() {
4 <-ch
5 }()
6}
7
8func main() {
9 for range 3 {
10 leak()
11 }
12 runtime.GC()
13 runtime.GC()
14 pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
15}
1goroutineleak profile: всего 3
23 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
3# 0x104e374f3 main.leak.func1+0x23 /tmp/go127/leak/main.go:12
Три горутины, постоянно заблокированные, с указанием файла и номера строки, где именно вы допустили ошибку.
Трюк за этим действительно остроумен: он повторно использует анализ достижимости сборщика мусора. Если горутина G заблокирована на примитиве P, и P недоступен ни из какой исполняемой горутины (или чего-либо, что эти горутины могли бы разбудить), то ничто не сможет коснуться P снова, поэтому G никогда не проснется. Это не эвристика — это доказательство.
Тот же дизайн дает ограничение бесплатно: если канал или мьютекс доступны через глобальную переменную или локальную переменную горутины, которая все еще выполняется, сборщик мусора все еще может их видеть, поэтому среда выполнения не может сделать никаких выводов. Заметьте, что даже в игрушечном примере выше требуется два вызова runtime.GC() для отчета. Он не поймает все.
Однако он поймает классическую ошибку «забыл отменить контекст, рабочая горутина живет вечно» — что является причиной большинства утечек в большинстве случаев.
Если вы импортируете net/http/pprof, это также доступно по адресу /debug/pprof/goroutineleak. Подключите это в стейджинге, проверяйте еженедельно и тихо ужасайтесь.
Эта работа была внесена Владом Сайоком из Uber, который заслуживает напитка на свой выбор.
2.3. Трассировки теперь сообщают, какой запрос завершился неудачно
Для модулей, объявляющих Go 1.27 или новее, строки заголовка трассировки теперь включают метки (labels) горутин из runtime/pprof.
1func handle(ctx context.Context) {
2 var wg sync.WaitGroup
3 wg.Go(func() {
4 buf := make([]byte, 4<<10)
5 os.Stdout.Write(buf[:runtime.Stack(buf, true)])
6 })
7 wg.Wait()
8}
9
10func main() {
11 labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12 pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3 /tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5 ...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...
Важная деталь: метки наследуются дочерними горутинами. Горутина 3 никогда не устанавливала метку. Она получила ее от родителя.
Поэтому в следующий раз, когда в проде произойдет взаимная блокировка (deadlock) и кто-то отправит SIGQUIT процессу, вместо 4000 идентичных горутин вы получите 4000 горутин, помеченных тем, к какому запросу и какому арендатору (tenant) они принадлежат. Три строки pprof.Do в точке входа вашего запроса дают вам этот результат.
Поскольку метки могут содержать данные, которые вы бы предпочли не выводить в stderr, GODEBUG=tracebacklabels=0 отключает это, и этот отказ явно предназначен для того, чтобы остаться навсегда.
2.4. asynctimerchan исчез навсегда
Go 1.23 сделал каналы таймеров небуферизованными (синхронными) и предложил asynctimerchan=1 как способ вернуться к старому поведению. В 1.27 эта настройка полностью удалена. Каналы пакета time синхронны, точка, обсуждению не подлежат.
Интересна политика, введенная вместе с этим. Удаленный GODEBUG, оставшийся в вашем go.mod, не ломает сборку автоматически — он ломает ее только в том случае, если установлен в старое значение:
1# go.mod содержит: godebug asynctimerchan=1
2go: error loading go.mod:
3go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)
4
5# go.mod содержит: godebug asynctimerchan=0
6(собирается нормально)
Это означает, что «ругают» только тех, кто действительно полагался на удаленное поведение. Все, кто установил его в конечное значение по умолчанию и забыл об этом, могут продолжать не думать об этом. Это продуманный фрагмент археологии API.
3. Стандартная библиотека
3.1. encoding/json/v2: они заменили движок прямо в полете
Это самое важное. Более двух лет обсуждения предложений наконец привели к результату.
Теперь существует три пакета, и понимание этого разделения — большая часть успеха:
| Пакет | Задача |
|---|---|
encoding/json | API v1, которое вы знаете. Поведение изменено на 0%. Теперь реализовано поверх v2 |
encoding/json/v2 | Семантическая обработка. Go-значения ↔ JSON |
encoding/json/jsontext | Синтаксическая обработка. JSON как поток токенов |
Главное — encoding/json был перестроен на совершенно другой реализации и ведет себя идентично. Вот как:
1// encoding/json.Unmarshal из Go 1.27, сокращенно
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Каждая особенность v1 была закодирована как опция, объединена в DefaultOptionsV1(), и API v1 всегда применяет этот пакет. Это означает, что поведение одинаково в 1.26 и 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Да, v1 по-прежнему молча принимает дубликаты ключей и берет последний. Так было всегда. Так есть и сейчас. Совместимость означает совместимость и с плохими частями тоже.
v2, тем временем, имеет свое мнение:
1var m map[string]int
2jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
3// jsontext: дублирующееся имя члена объекта "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: недопустимый UTF-8 после смещения 1
Дублирующиеся ключи и недопустимый UTF-8 отклоняются. Оба варианта действительно опасны, а не просто неопрятны — когда два парсера расходятся во мнениях относительно того, какой дубликат ключа побеждает, возникают уязвимости безопасности. CVE-2017-12635 в CouchDB была именно такой: тело JSON с двумя ключами roles, где валидатор читал один, а слой хранения — другой. Отклонение — правильное решение.
Опции являются вариативными:
1type Config struct {
2 Name string `json:"name"`
3 Tags []string `json:"tags,omitzero"`
4 Timeout int `json:"timeout,omitzero"`
5}
6
7out, _ := jsonv2.Marshal(Config{Name: "api"})
8// {"name":"api"}
9
10// отсортированные ключи карты + отступы
11out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
12 jsonv2.Deterministic(true), jsontext.WithIndent(" "))
13// {
14// "a": 1,
15// "b": 2
16// }
17
18// отклонять неизвестные поля — ранее требовало Decoder и DisallowUnknownFields()
19var c Config
20err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
21 jsonv2.RejectUnknownMembers(true))
22// json: невозможно распаковать JSON-строку в Go main.Config:
23// неизвестное имя члена объекта "nope"
Deterministic, MatchCaseInsensitiveNames, StringifyNumbers, FormatNilSliceAsNull, OmitZeroStructFields — большинство вещей, которые вы ранее решали с помощью сторонней библиотеки или рукописного MarshalJSON, теперь являются флагами.
И история миграции — лучшая часть. Последние опции побеждают, поэтому вы можете внедрять строгость v2 поведение за поведением:
1// сохранить семантику v1, но отклонять дубликаты ключей, как это делает v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// дублирующееся имя члена объекта
Вам не нужно портировать кодовую базу в 200 тысяч строк на v2, чтобы перестать принимать дубликаты ключей. Вы меняете одну опцию. Для всего крупного это реалистичный путь.
Производительность: маршалинг примерно на одном уровне, распаковка значительно быстрее. Если что-то идет не так, GOEXPERIMENT=nojsonv2 восстанавливает старую реализацию — и эта возможность отказа тоже когда-нибудь будет удалена, поэтому лучше создайте тикет, чем привыкать.
jsontext — это низкоуровневый слой: Encoder/Decoder, обходящие JSON как Tokenы и Value с конечным автоматом, который держит вас в рамках. Используйте его, когда пишете потоковый трансформатор или JSON-фильтр и не хотите вообще материализовать Go-значения.
3.2. Стандартный пакет uuid
Наконец-то. RFC 9562 в стандартной библиотеке, go get не требуется.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: верхние 48 бит — это временная метка, поэтому они всегда сортируются в порядке создания
8 a, b := uuid.NewV7(), uuid.NewV7()
9 fmt.Println(a.Compare(b) < 0) // true
10
11 u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
12 fmt.Println(u, err)
13 // 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>
14
15 fmt.Println(uuid.Nil(), uuid.Max())
16 // 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
17}
Весь API — это New, NewV4, NewV7, Nil, Max, Parse, MustParse и один тип: type UUID [16]byte. Вот и все. Вы можете прочитать всю документацию пакета, пока остывает ваш кофе.
Детали, которые стоит знать:
UUID— это[16]byte, поэтому==работает, и его можно напрямую использовать как ключ карты. Тот же дизайн, что и вgoogle/uuid.NilиMax— функции, а не переменные. Потому что переменнаяNil UUIDна уровне пакета — это заряженное ружье, направленное вам в ногу, и кто-нибудь, когда-нибудь, обязательно попытался бы присвоить ей значение.- Случайные биты берутся из криптографически стойкого генератора.
- Он реализует
encoding.TextMarshaler/TextUnmarshaler/TextAppender, поэтому он сразу вставляется в JSON-структуры. NewV7— это то, что вам нужно для первичных ключей базы данных. Временная упорядоченность означает, что ваш B-tree индекс перестанет фрагментироваться, в чем UUID v4 печально известны.
Теперь вы можете удалить зависимость. Если вам нужны v1/v3/v5 или более сложные варианты парсинга, сторонние библиотеки все еще востребованы.
3.3. crypto/mldsa: постквантовые подписи, и они огромны
Go 1.24 дал нам crypto/mlkem для постквантового обмена ключами. Go 1.27 приносит вторую половину: подписи ML-DSA, стандартизированные как FIPS 204.
1sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
2pk := sk.PublicKey()
3
4msg := []byte("release the gophers")
5opts := &mldsa.Options{Context: "gosuda.org/blog"}
6
7sig, _ := sk.Sign(rand.Reader, msg, opts)
8
9fmt.Println("seed закрытого ключа:", len(sk.Bytes()), "байт") // 32
10fmt.Println("открытый ключ:", len(pk.Bytes()), "байт") // 1952
11fmt.Println("подпись:", len(sig), "байт") // 3309
12
13fmt.Println(mldsa.Verify(pk, msg, sig, opts))
14// <nil>
15fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
16// mldsa: недопустимая подпись
Посмотрите на эти цифры. Одна подпись — 3309 байт. Ed25519 — 64. Это увеличение в 50 раз, а открытый ключ занимает почти 2 КБ сверху. Запихните несколько таких в цепочку сертификатов, и вашему рукопожатию TLS потребуется собственная стратегия MTU.
Это реальная цена квантовой устойчивости сегодня, и именно поэтому никто не переходит на все это завтра. Но теперь это в стандартной библиотеке, где вы и хотите это видеть до того, как это станет необходимо.
Options.Context — это разделение доменов: подписывайте одним ключом для разных целей, используйте разный контекст для каждой, и подпись из одного контекста не пройдет проверку в другом. Пример выше показывает именно это — тот же ключ, то же сообщение, другой контекст, отклонено.
PrivateKey реализует crypto.Signer, поэтому он вписывается в существующие интерфейсы. crypto/x509 обрабатывает ключи и подписи ML-DSA, а crypto/tls поддерживает схемы подписи MLDSA44/MLDSA65/MLDSA87 в TLS 1.3.
Также есть SignDeterministic, которая пропускает случайность — удобно для тестов и воспроизводимых сборок.
3.4. simd: векторные инструкции без ассемблера
Go 1.26 представил экспериментальный simd/archsimd, специфичный для архитектуры. Go 1.27 добавляет simd — портативный и не зависящий от ширины вектора. Включите с помощью GOEXPERIMENT=simd.
1// dst = a*x + y
2func axpy(dst, x, y []float32, a float32) {
3 va := simd.BroadcastFloat32s(a)
4 w := va.Len()
5
6 i := 0
7 for ; i+w <= len(x); i += w {
8 vx := simd.LoadFloat32s(x[i:])
9 vy := simd.LoadFloat32s(y[i:])
10 vx.MulAdd(va, vy).Store(dst[i:])
11 }
12 // обработка хвоста частичной загрузкой/выгрузкой — без скалярного цикла очистки
13 if i < len(x) {
14 vx, _ := simd.LoadFloat32sPart(x[i:])
15 vy, _ := simd.LoadFloat32sPart(y[i:])
16 vx.MulAdd(va, vy).StorePart(dst[i:])
17 }
18}
1$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
2векторных бит: 128 эмулировано: false
3[4 7 10 13 16 19 22 25 28 31]
Дизайнерское решение, которое имеет значение: ширина вектора никогда не захардкожена. Вы запрашиваете va.Len() во время выполнения. VectorBitSize() говорит вам фактическую ширину, Emulated() говорит, получили ли вы реальное оборудование или вежливую программную имитацию. Мой Mac сообщил о 128-битном NEON; машина с AVX-512 сообщает о большем; машина без поддержки сообщает об эмуляции, и код все равно работает.
LoadFloat32sPart/StorePart заслуживают особого упоминания. Самая раздражающая часть рукописного SIMD — это всегда неровный хвост в конце массива, и это решение обрабатывает его без отдельного скалярного цикла.
Все еще экспериментально, API все еще нестабильно, не используйте в проде. Но тот факт, что это код на Go, а не ассемблер или cgo, — действительно большое дело.
3.5. hash/maphash.Hasher
Новый интерфейс, описывающий контракт между типом значения и контейнерами на основе хешей:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Почему? Встроенная карта Go принимает только comparable ключи. Вы не можете использовать срез как ключ. Вы не можете определить «равенство без учета регистра». Hasher решает обе проблемы сразу:
1type CaseInsensitive struct{}
2
3func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
4 h.WriteString(strings.ToLower(s))
5}
6func (CaseInsensitive) Equal(x, y string) bool {
7 return strings.ToLower(x) == strings.ToLower(y)
8}
9
10var seed = maphash.MakeSeed()
11
12func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
13 var h maphash.Hash
14 h.SetSeed(seed) // тот же seed, иначе все это не имеет смысла
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Для обычной семантики == есть ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Подвох: Hasher — это интерфейс, а не структура данных. Пока нет стандартной хеш-таблицы или фильтра Блума, который бы его потреблял. Это фундамент для будущего пакета контейнеров. Сегодня вы используете его при создании собственной структуры или при использовании существующей реализации, например go/types.Hasher (которая позволяет использовать types.Type как ключ карты, соблюдая Identical).
Управление seed — на вас. Если бы hashOf выше создавал новый seed при каждом вызове, идентичные значения имели бы разные хеши, и пример вывел бы false — именно ту ошибку, которую я совершил при первой попытке. Один seed на контейнер. (Seed рандомизируется для предотвращения DoS-атак на хеш-таблицы, поэтому он не является константой).
3.6. httptest.NewTestServer + synctest.Sleep: хит сезона
Если вы возьмете из этого релиза только одно, пусть это будет оно.
testing/synctest стал стабильным в Go 1.25 и дал нам фиктивные часы для тестирования конкурентного кода. Но в момент, когда ваш тест касался реальной сети, иллюзия рушилась. httptest.NewTestServer в Go 1.27 использует фиктивную сеть в памяти, поэтому пузырь остается целым. А synctest.Sleep (= time.Sleep + synctest.Wait) дополняет картину.
Вот тест повторных попыток с экспоненциальной задержкой:
1func TestRetryWithBackoff(t *testing.T) {
2 synctest.Test(t, func(t *testing.T) {
3 var hits int
4 srv := httptest.NewTestServer(t, http.HandlerFunc(
5 func(w http.ResponseWriter, r *http.Request) {
6 hits++
7 if hits < 3 {
8 w.WriteHeader(http.StatusServiceUnavailable)
9 return
10 }
11 io.WriteString(w, "ok")
12 }))
13
14 client := srv.Client()
15 start := time.Now()
16 var body string
17 for attempt := range 5 {
18 resp, err := client.Get(srv.URL)
19 if err != nil {
20 t.Fatal(err)
21 }
22 b, _ := io.ReadAll(resp.Body)
23 resp.Body.Close()
24 if resp.StatusCode == http.StatusOK {
25 body = string(b)
26 break
27 }
28 synctest.Sleep(time.Duration(1<<attempt) * time.Second)
29 }
30
31 if body != "ok" {
32 t.Fatalf("got %q", body)
33 }
34 // задержка должна быть ровно 1с + 2с = 3с
35 if elapsed := time.Since(start); elapsed != 3*time.Second {
36 t.Fatalf("elapsed = %v, want 3s", elapsed)
37 }
38 t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
39 })
40}
1=== RUN TestRetryWithBackoff
2 x_test.go:48: hits=3 elapsed=3s
3--- PASS: TestRetryWithBackoff (0.00s)
Прочитайте это дважды. Он имитировал три секунды задержки против реального HTTP-сервера и завершился за 0.00 секунд. И утверждение elapsed == 3*time.Second — ровно три секунды, а не «минимум три секунды, плюс-минус дрожание планировщика». Фиктивные часы не дрожат.
synctest.Sleep существует по конкретной причине: если ваш тест спит столько же времени, сколько тестируемый код, кто проснется первым — неизвестно. synctest.Sleep засыпает, а затем ждет, пока каждая другая горутина в пузыре не окажется надежно заблокированной, поэтому вы наблюдаете систему после того, как она стабилизировалась.
Тайм-ауты, повторные попытки, выключатели, ограничители скорости — каждый зависимый от времени тест HTTP-клиента, который у вас есть, может стать быстрым и детерминированным. Если ваш набор тестов в настоящее время держится на time.Sleep(100 * time.Millisecond) и надежде, это ваш выход.
3.7. Изменения в net/http, которые действительно влияют на прод
Тихие, но они проявятся в ваших метриках.
Тела ответов HTTP/1 автоматически очищаются при Close. Непрочитанный контент теперь сливается (до консервативного предела) при закрытии тела, поэтому соединение можно использовать повторно. Это означает, что вы наконец можете удалить это заклинание, которое все копипастили из одного и того же ответа на Stack Overflow с 2016 года:
1// больше не нужно
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Для большинства программ это либо ничего не меняет, либо дает небольшую выгоду. Если это делает вещи хуже, вы, вероятно, находитесь в группе, которую примечания к выпуску вежливо описывают: Transport.MaxIdleConns установлено в 0, или новый Client на каждый запрос, полностью обходящий лимит простаивающих соединений. Transport.DisableKeepAlives = true замаскирует это, но реальный совет примечаний к выпуску заключается в том, что «более глубокий взгляд был бы полезен», что на языке команды Go означает у вас проблемы посерьезнее.
Приоритет клиента HTTP/2 (RFC 9218). Сервер теперь учитывает сигналы приоритета клиента. Если вы предпочитали старое планирование round-robin, используйте Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Ограничивает количество значений заголовков, которые сервер примет, по умолчанию DefaultMaxHeaderValueCount. Еще одна дверь закрыта для атаки «отправь десять тысяч заголовков и посмотри, что будет».
ALPN на предоставленных пользователем соединениях. Если ваш net.Conn реализует ConnectionState() tls.ConnectionState, Transport и Server будут выполнять согласование TLS ALPN на нем — поэтому пользовательские диалеры, проходящие через прокси, все равно могут согласовать HTTP/2.
3.8. Мелочи, которые радуют
strings.CutLast / bytes.CutLast. Cut разделяет по первому разделителю. Варианта «последний» не было, поэтому все писали LastIndex плюс нарезку, и примерно 30% из нас ошибались на единицу с первой попытки.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Отлично подходит для расширений файлов и для парсинга host:port — где вы должны найти последнее двоеточие, потому что IPv6-адреса полны ими.
math/big.Int.Divide. Деление с явным режимом округления, заменяющее подбрасывание монетки «мне нужно Quo или Div?»:
1x, y := big.NewInt(-7), big.NewInt(2)
2
3new(big.Int).Divide(x, y, new(big.Int), big.Trunc) // q=-3 r=-1
4new(big.Int).Divide(x, y, new(big.Int), big.Floor) // q=-4 r=1
5new(big.Int).Divide(x, y, new(big.Int), big.Round) // q=-4 r=1
6new(big.Int).Divide(x, y, new(big.Int), big.Ceil) // q=-3 r=-1
Если вы работаете в области, где правило округления прописано в регламенте, это для вас.
url.URL.Clone и url.Values.Clone. Глубокие копии. Старая поверхностная копия *u разделяла указатель Userinfo, что приводило именно к тому типу ошибок, на поиск которых уходит день, а на исправление — пять секунд.
1orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
2clone := orig.Clone()
3clone.Host = "example.com"
4fmt.Println(orig.Host, clone.Host) // gosuda.org example.com
Методы чтения net.UnixConn теперь возвращают io.EOF напрямую, а не оборачивают его в net.OpError. err == io.EOF теперь работает, как и должно было всегда.
database/sql.ConvertAssign и driver.RowsColumnScanner. Функции для авторов драйверов. Первая раскрывает преобразования типов, которые выполняет Rows.Scan; вторая позволяет драйверам сканировать данные напрямую в места назначения пользователя, пропуская промежуточное выделение памяти.
unicode 15 → 17. Две версии за один прыжок. Классификация строк и поведение нормализации могут слегка измениться. Если у вас есть тесты, которые от этого зависят, вы это обнаружите.
compress/flate стал быстрее — и его выходные байты могут отличаться от Go 1.26. Это каскадом переходит на archive/zip, compress/gzip, compress/zlib и image/png. Если у вас есть тесты «золотых файлов», которые хешируют сжатый вывод, они сломаются, и это не будет регрессией. Проверьте это до обновления, а не во время разбора инцидента.
4. Набор инструментов
4.1. go fix получил больше модернизаторов
go fix тихо превращается в инструмент модернизации кода. Go 1.27 добавляет atomictypes, embedlit, slicesbackward и unsafefuncs. Используйте -diff для предпросмотра:
1package fixdemo
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Base struct{ ID int }
9type User struct {
10 Base
11 Name string
12}
13
14func New() User {
15 return User{Base: Base{ID: 1}, Name: "gopher"}
16}
17
18var counter int64
19
20func Incr() { atomic.AddInt64(&counter, 1) }
21
22func Reverse(s []string) {
23 for i := len(s) - 1; i >= 0; i-- {
24 _ = s[i]
25 }
26}
27
28func Spawn(wg *sync.WaitGroup) {
29 wg.Add(1)
30 go func() {
31 defer wg.Done()
32 }()
33}
1$ go1.27rc3 fix -diff ./fixdemo
1 import (
2+ "slices"
3 "sync"
4 "sync/atomic"
5 )
6
7 func New() User {
8- return User{Base: Base{ID: 1}, Name: "gopher"}
9+ return User{ID: 1, Name: "gopher"}
10 }
11
12-var counter int64
13+var counter atomic.Int64
14
15-func Incr() { atomic.AddInt64(&counter, 1) }
16+func Incr() { counter.Add(1) }
17
18 func Reverse(s []string) {
19- for i := len(s) - 1; i >= 0; i-- {
20- _ = s[i]
21+ for _, v := range slices.Backward(s) {
22+ _ = v
23 }
24 }
25
26 func Spawn(wg *sync.WaitGroup) {
27- wg.Add(1)
28- go func() {
29- defer wg.Done()
30- }()
31+ wg.Go(func() {
32+ })
33 }
Четыре модернизатора сработали на одном маленьком файле:
embedlit— переписывает встроенные литералы, используя новый синтаксис из §1.2atomictypes—atomic.AddInt64(&x, 1)становитсяatomic.Int64.Add(1), что делает невозможным неатомарный доступ и тихо исправляет ошибки выравнивания 32-битных значений, о которых вы не подозревалиslicesbackward— обратные циклы становятсяslices.Backwardwaitgroupgo— ритуалAdd/go/Doneстановитсяwg.Go(переименовано изwaitgroupверсии 1.26 во избежание двусмысленности)
go tool fix help перечисляет все 26; go tool fix help <name> объясняет один. Кроме того, fmtappendf был удален «из-за стилистических соображений», что является прекрасно дипломатичным способом описать то, что произошло в той ветке обсуждения.
Запуск go fix ./... на старой кодовой базе — это по-настоящему приятное занятие. Конечно, сначала прочитайте diff.
4.2. go test по умолчанию запускает stdversion
Это изменение с наибольшей вероятностью прервет ваш день.
go test теперь по умолчанию запускает проверку stdversion, помечая символы стандартной библиотеки, которые новее, чем позволяет директива go в вашем go.mod:
1// go.mod говорит: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast — символ 1.27
8 return ext
9}
1$ go1.27rc3 test ./...
2# sv
3./x.go:6:23: strings.CutLast требует go1.27 или новее (модуль go1.24)
4FAIL sv [сборка не удалась]
Это убивает классический режим сбоя, когда все работает на вашей машине (последний набор инструментов) и взрывается в CI или в старой среде пользователя. Если вы публикуете библиотеки с консервативной директивой go, это подарок.
4.3. go test -json получил OutputType
Строки "Action":"output" теперь несут необязательное поле "OutputType": "error", "error-continue" или "frame".
1'frame' '=== RUN TestFail\n'
2None ' x_test.go:6: hello\n'
3'error' ' x_test.go:7: boom\n'
4'frame' '--- FAIL: TestFail (0.00s)\n'
5'frame' 'FAIL\n'
Вывод t.Log (нет поля), вывод t.Error (error) и строки, сгенерированные фреймворком (frame), теперь различимы. Любой, кто писал регулярное выражение, чтобы понять, какие строки вывода теста являются «реальными», теперь может его удалить.
4.4. Улучшения go doc
Синтаксис package@version. Читайте документацию для конкретной версии, не добавляя ее в свой модуль:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Удобно для проверки того, что изменилось перед обновлением.
-ex и печать исходного кода примеров:
1$ go1.27rc3 doc -ex strings | grep Example
2 func ExampleClone()
3 func ExampleCompare()
4 func ExampleContains()
5 func ExampleCut()
6 func ExampleCutPrefix()
7 ...
8
9$ go1.27rc3 doc strings.ExampleCut
10package main
11
12import (
13 "fmt"
14 "strings"
15)
16
17func main() {
18 show := func(s, sep string) {
19 before, after, found := strings.Cut(s, sep)
20 fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
21 }
22 show("Gopher", "Go")
23 ...
24}
25
26Output:
27Cut("Gopher", "Go") = "", "pher", true
Исходный код примера и ожидаемый вывод, прямо в терминале, без открытия вкладки браузера, которая будет висеть до следующего четверга.
4.5. go mod tidy очищает блоки require
Для модулей на go 1.27 или новее, go mod tidy объединяет разрозненные блоки require максимум в два (прямые и косвенные), сохраняя прикрепленные комментарии.
1// До
2module tidydemo
3
4go 1.27
5
6require golang.org/x/sync v0.10.0
7
8// networking
9require golang.org/x/net v0.33.0
10
11require (
12 golang.org/x/sys v0.28.0 // indirect
13)
14
15require golang.org/x/text v0.21.0 // indirect
1// После: go mod tidy
2module tidydemo
3
4go 1.27
5
6require (
7 // networking
8 golang.org/x/net v0.33.0
9 golang.org/x/sync v0.10.0
10)
Обратите внимание, что комментарий // networking сохранился. Это в основном очищает последствия разрешения конфликтов слияния Git, где рождаются лишние блоки require. Если в вашей команде более трех человек добавляют зависимости, ваши конфликты go.mod скоро станут заметно тише.
4.6. Разное
- Поддержка
bzrудалена. Если это затрагивает вас, я бы искренне хотел услышать эту историю. go tool trace -httpпривязывается к localhost, когда указан только порт.-http=:6060больше не слушает на всех интерфейсах. Используйте-http=0.0.0.0:6060, если вы это имели в виду. Теперь это соответствуетgo tool pprofи предотвращает случайные публичные профилировщики.- Файлы ответов (
@file) поддерживаютсяcompile,link,asm,cgo,coverиpackв формате, совместимом с GCC. Для систем сборки, которые превышают лимиты длины командной строки — привет, Bazel.
5. Компилятор, линковщик, порты
Компилятор. Относительные имена файлов в директивах //line теперь разрешаются относительно каталога содержащего файла, соответствуя go/scanner. Актуально, если вы пишете генераторы кода.
Имена символов функциональных литералов (замыканий) теперь тоже проще — одно и то же имя независимо от встраивания (inlining), и несколько экземпляров одного и того же литерала могут разделять код в бинарном файле. Функциональных изменений нет, кроме: код, сравнивающий идентичность функций через reflect.Value.Pointer, будет видеть «равенство» чаще, чем раньше. Это сравнение никогда не было валидным, но если оно у вас есть, сейчас оно начнет лгать вам громче.
Линковщик. Новые опции -macos и -macsdk устанавливают версии ОС и SDK в команде загрузки LC_BUILD_VERSION в macOS.
Порты.
- Darwin теперь требует macOS 13 Ventura или новее, как было объявлено в 1.26. Проверьте свои CI-раннеры.
- PowerPC (
GOOS=linux GOARCH=ppc64) переключился на ABI ELFv2. Требуется ядро Linux 3.13+ (RHEL7 бэкпортировано до 3.10). Cgo, PIE и внешняя линковка теперь поддерживаются. Если вы используете cgo, но вам нужен статический чистый Go-бинарник, установитеCGO_ENABLED=0.
6. Чек-лист обновления
Go 1.27 серьезно относится к совместимости, но проверьте следующее перед повышением версии:
- Тесты золотых файлов на сжатом выводе.
compress/flateизменил кодировщики; байты gzip/zip/png могут отличаться. - Тесты, соответствующие строкам сообщений об ошибках JSON. Поведение идентично; текст ошибки — нет.
- Удаленные GODEBUG в
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— они ломают сборку только если установлены в свои старые значения. - Нарушения
stdversion.go testтеперь ловит их. Запускайте рано, если поддерживаете библиотеку с консервативной директивойgo. - CI-раннеры macOS 12 или старше. Поддержка удалена.
- Тесты, утверждающие имена символов функциональных литералов, и сравнения функций через
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0илиClientна каждый запрос. В сочетании с автосливом тел ответов это может стать медленнее.
Заключительные мысли
Go 1.27 — это релиз, в котором накопилось много отложенного обслуживания.
Обобщенные методы были недостающим элементом работы с обобщениями 1.18. Селекторы полей в литералах структур закрыли одиннадцатилетнюю проблему. encoding/json/v2 — это два года обсуждения предложений. А uuid заполнил дыру, которую практически каждый проект на Go затыкал одной и той же сторонней зависимостью в течение десятилетия.
Если вам нужен приоритет для получения пользы:
httptest.NewTestServer+synctest.Sleep— зависящие от времени HTTP-тесты становятся быстрыми и детерминированными. Лучшее соотношение усилий и выгоды во всем релизе.- Профиль утечек горутин — откройте один эндпоинт в стейджинге и готовьтесь к смирению.
- Метки горутин в трассировке — три строки
pprof.Doв точке входа вашего запроса превращают ваш следующий стек-дамп в 3 часа ночи во что-то читаемое. go fix ./...— позвольте инструменту сделать модернизацию.encoding/json/v2— не спешите. Внедряйте по одной опции за раз.
Хватайте go1.27rc3 и запустите свой набор тестов прямо сейчас. Все, что есть в этом чек-листе, значительно приятнее обнаружить во вторник днем, чем во время инцидента.
Ссылки