Go 1.27, или Как я научился не волноваться и полюбить обобщённые методы


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 у нас есть дженерики, и с Go 1.18 существует и «The Conversation». Она выглядит так:
«Давайте я просто напишу метод
Mapдля моего типа-слайса—»
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: обобщённый метод имеет бесконечно много инстанцирований, а таблица методов интерфейса должна быть конечной и известной на этапе компиляции. Нельзя поместить бесконечное в конечную таблицу. Вселенная сказала «нет».
Практический вывод: обобщённые методы предназначены для конкретных типов с 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, которая использовала глобальный источник. Теперь ваш собственный инициализированный *Rand получает то же удобство. Мелочь, но приятная.
1.2. Селекторы полей в литералах структур, или: issue #9859 наконец-то закрыта
Встраивание даёт вам u.ID. Встраивание не даёт User{ID: 1}. Вместо этого оно требует User{Base: Base{ID: 1}}, что является способом Go спросить, действительно ли вы это имели в виду.
Issue #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"}, ...}.
Теперь о мелком шрифте, который я проверил, действительно попытавшись скомпилировать весь код.
(a) Ключом по-прежнему является простой идентификатор. Нельзя записать произвольный путь селектора.
1_ = Line{Object.name: "x"} // invalid field name Object.name in struct literal
2_ = Line{p.x: 1} // invalid field name p.x in struct literal
Поэтому функция означает не «ключи теперь работают как доступ к полю», а именно «разрешены неявно продвинутые имена полей». Это несколько уже, чем подсказывает заголовок.
(b) Нельзя одновременно указать встроенное поле и поле, продвинутое из него.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Разумно. Вы сообщили компилятору два противоречивых факта об одной и той же памяти, и он отказался угадывать.
(c) Встраивание указателей не поддерживается.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach 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. Аллокация, специализированная по размеру
Компилятор теперь генерирует вызовы подпрограмм аллокации, специализированных по размеру, для маленьких объектов. В примечаниях к выпуску заявлено до 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 ns/op
3BenchmarkSlice-12 3000000 13.58 ns/op
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12 3000000 8.722 ns/op
7BenchmarkSlice-12 3000000 20.81 ns/op
8
На 30–35% быстрее в микробенчмарке, который ничего не делает, кроме аллокаций, на Apple Silicon. Иными словами: это лучший случай, достигнутый в лабораторных условиях бенчмарком, созданным для того, чтобы цифра выглядела хорошо. В реальной программе сборка мусора и реальная работа поглотят большую часть выигрыша. Утверждение о ~1% является честным.
Ценой является размер бинарного файла. Hello World:
12413202 bytes (default)
22361826 bytes (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: total 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. Подключите его к staging, проверяйте раз в неделю и тихо ужасайтесь.
Эта работа была внесена Vlad Saioc из Uber, которому полагается напиток на их выбор.
2.3. Трассировки теперь сообщают, какой запрос упал
Для модулей, объявляющих Go 1.27 или новее, строки заголовков трассировки теперь включают метки горутин 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 никогда не устанавливала метку. Она получила её от своего родителя.
Поэтому в следующий раз, когда в продакшене случится дедлок и кто-то отправит процессу SIGQUIT, вместо 4000 горутин, которые все выглядят одинаково, вы получите 4000 горутин с пометкой о том, какому запросу и какому тенанту они принадлежат. Три строки 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, который вы знаете. Поведение на 100% не изменилось. Теперь реализован поверх 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: duplicate object member name "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: invalid UTF-8 after offset 1
Повторяющиеся ключи и некорректный UTF-8 отвергаются. Оба случая действительно опасны, а не просто неряшливы: когда два парсера расходятся во мнениях о том, какой повторяющийся ключ побеждает, возникают ошибки безопасности. CVE-2017-12635 в CouchDB был именно таким: тело JSON с двумя ключами roles, где валидатор читал один, а слой хранения другой. Отказ является правильным решением.
Опции передаются как variadic-параметры:
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// сортировка ключей map и отступы
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: cannot unmarshal JSON string into Go main.Config:
23// unknown object member name "nope"
Deterministic, MatchCaseInsensitiveNames, StringifyNumbers, FormatNilSliceAsNull, OmitZeroStructFields — большинство вещей, которые раньше решались сторонней библиотекой или самописным MarshalJSON, теперь являются флагами.
А лучшая часть — история миграции. Более поздние опции имеют приоритет, поэтому вы можете внедрять строгость v2 по одному поведению за раз:
1// сохранить семантику v1, но отклонять повторяющиеся ключи, как это делает v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Не нужно портировать кодовую базу в 200 тысяч строк на v2, чтобы перестать принимать повторяющиеся ключи. Вы переключаете одну опцию. Для крупных проектов это реалистичный путь.
Производительность: маршалинг примерно на уровне прежнего, демаршалинг значительно быстрее. Если что-то пойдёт не так, GOEXPERIMENT=nojsonv2 восстанавливает старую реализацию, и этот вариант отказа тоже рано или поздно будет удалён, поэтому заводите issue, а не смиряйтесь.
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, поэтому работает==, и его можно напрямую использовать как ключ map. Тот же дизайн, что и уgoogle/uuid.NilиMaxявляются функциями, а не переменными. Потому что пакетнаяvar Nil UUID— это заряженное ружьё, направленное на вашу ногу, и кто-то, где-то однажды обязательно присвоил бы ей значение.- Случайные биты берутся из криптографически стойкого генератора.
- Он реализует
encoding.TextMarshaler/TextUnmarshaler/TextAppender, поэтому легко встраивается в структуры JSON. NewV7— именно то, что нужно для первичных ключей баз данных. Упорядочивание по времени означает, что ваш индекс B-дерева перестаёт фрагментироваться, с чем 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("private key seed:", len(sk.Bytes()), "bytes") // 32
10fmt.Println("public key:", len(pk.Bytes()), "bytes") // 1952
11fmt.Println("signature:", len(sig), "bytes") // 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: invalid signature
Посмотрите на эти числа. Одна подпись занимает 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
2vector bits: 128 emulated: 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}
Зачем? Встроенный map 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 как ключ map с учётом Identical).
Управление seed лежит на вас. Если бы hashOf выше создавал новый seed при каждом вызове, одинаковые значения хешировались бы по-разному, и пример выводил бы false — именно эту ошибку я допустил в первой попытке. Один seed на контейнер. (Seed randomизируется, чтобы защититься от DoS-атак на переполнение хешей, поэтому это не просто константа.)
3.6. httptest.NewTestServer + synctest.Sleep: удар по «спящему»
Если вы возьмёте из этого выпуска только одну вещь, пусть это будет она.
testing/synctest вышел из экспериментального статуса в Go 1.25 и дал нам фальшивые часы для тестирования конкурентного кода. Однако как только тест касался реальной сети, иллюзия рушилась. httptest.NewTestServer в Go 1.27 использует in-memory фейковую сеть, поэтому «пузырь» остаётся цельным. А 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. Если у вас есть golden-file тесты, которые хешируют сжатый вывод, они сломаются, и это не будет регрессией. Проверьте это до обновления, а не на разборе инцидента.
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(переименован изwaitgroup1.26, чтобы избежать неоднозначности)
go tool fix help перечисляет все 26; go tool fix help <name> объясняет один из них. Кроме того, fmtappendf был удалён «из-за стилистических соображений», что является удивительно дипломатичным способом описать всё, что произошло в той ветке обсуждения.
Запуск go fix ./... на старой кодовой базе — это действительно приятный день. Сначала, разумеется, изучите diff.
4.2. go test по умолчанию запускает stdversion
Это изменение, которое с наибольшей вероятностью прервёт ваш день.
go test теперь по умолчанию запускает проверку stdversion из vet, отмечая символы стандартной библиотеки, которые новее, чем позволяет директива 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 requires go1.27 or later (module is go1.24)
4FAIL sv [build failed]
Это устраняет классический сценарий сбоя, когда всё работает на вашей машине (с последним инструментарием), а в 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. Важно, если вы пишете генераторы кода.
Имена символов для функциональных литералов (замыканий) тоже стали проще: одно и то же имя независимо от инлайнинга, и несколько экземпляров одного литерала могут разделять код в бинарном файле. Функциональных изменений нет, за исключением: код, сравнивающий идентичность функций через reflect.Value.Pointer, будет чаще видеть «равенство», чем раньше. Такое сравнение никогда не было корректным, но если оно у вас есть, теперь оно начнёт обманывать вас громче.
Линкер. Новые опции -macos и -macsdk задают версии ОС и SDK в команде загрузки macOS LC_BUILD_VERSION.
Порты.
- 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 серьёзно относится к совместимости, но проверьте следующее, прежде чем повышать версию:
- Golden-file тесты на сжатом выводе.
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-тесты становятся быстрыми и детерминированными. Лучшее соотношение усилий и отдачи во всём релизе.- Профиль утечек горутин — откройте один эндпоинт в staging и приготовьтесь к смирению.
- Метки горутин в трассировках — три строки
pprof.Doв точке входа запроса превращают ваш следующий дамп стека в 3 часа ночи во что-то читаемое. go fix ./...— пусть инструмент займётся модернизацией.encoding/json/v2— без спешки. Внедряйте по одной опции за раз.
Возьмите go1.27rc3 и прогоните свой тестовый набор прямо сейчас. Всё из этого контрольного списка гораздо приятнее обнаружить во вторник после обеда, чем во время инцидента.
Ссылки