---
id: 1a31e06c0d969b4b6cbbc590259cf089
author: iwanhae
title: Go 1.27, или Как я научился не волноваться и полюбить обобщённые методы
description: Обобщённые методы, encoding/json/v2, стандартный пакет uuid и детектор утечек горутин, который действительно работает. Обзор Go 1.27 с примерами, все из которых были фактически запущены.
language: ru
date: 2026-08-15T02:08:43.129534158Z
path: /blog/posts/go-1-27-or-how-i-learned-to-stop-worrying-and-love-generic-methods-za800e454
category: release
---

![Overview of Go 1.27](/assets/images/whats-new-in-go1.27/overview.webp)

Go 1.27 выходит в августе 2026 года, и на этот раз сам язык действительно изменился. Не «мы добавили новую функцию в `slices`», а именно изменился. Обобщённые методы (generic methods) наконец появились. Задача одиннадцатилетней давности закрыта. `encoding/json` был тихо заменён новым движком, пока самолёт находился в полёте.

Каждый пример ниже был запущен на `go1.27rc3` (darwin/arm64). Каждый блок вывода является настоящим выводом, скопированным без изменений, включая сообщения об ошибках. Если что-то выглядит странно, то это потому, что именно так ответил компилятор.

```bash
go install golang.org/dl/go1.27rc3@latest
go1.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 исправляет это:

```go
type List[E any] []E

// F — параметр типа, объявленный самим методом
func (l List[E]) Apply[F any](f func(E) F) List[F] {
	r := make(List[F], len(l))
	for i, x := range l {
		r[i] = f(x)
	}
	return r
}

func main() {
	l := List[int]{1, 2, 3}
	fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
	// [10 20 30]
}
```

Получатель даже не обязан быть обобщённым. Обычная структура может иметь обобщённый метод:

```go
type Bag struct{ items []any }

func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
```

Однако прежде чем рефакторить всю кодовую базу сегодня после обеда, стоит учесть **два ограничения**, и они важнее, чем кажутся:

1. Методы интерфейсов не могут объявлять параметры типа.
2. Обобщённые методы не могут реализовывать методы интерфейсов.

Именно второе ограничение создаст вам проблемы:

```go
type Adder interface{ Add(int) int }

type G struct{}

func (G) Add[T any](v T) T { return v }

var _ Adder = G{}
```

```
cannot use G{} (value of struct type G) as Adder value in variable declaration:
	G does not implement Adder (wrong type for method Add)
		have Add[T any](T) T
		want Add(int) int
```

Таким образом, обобщённые методы и динамическая диспетчеризация не сочетаются. Это не скупость команды Go: обобщённый метод имеет бесконечно много инстанцирований, а таблица методов интерфейса должна быть конечной и известной на этапе компиляции. Нельзя поместить бесконечное в конечную таблицу. Вселенная сказала «нет».

Практический вывод: обобщённые методы предназначены для конкретных типов с API утилитарного характера. Всё, что должно скрываться за интерфейсом, по-прежнему требует старого подхода.

Стандартная библиотека уже воспользовалась этим. У `Rand` из `math/rand/v2` появился обобщённый метод:

```go
func (r *Rand) N[Int intType](n Int) Int
```

```go
r := rand.New(rand.NewPCG(1, 2))
fmt.Println(r.N(int32(100)))     // 76
fmt.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](https://go.dev/issue/9859) был открыт в 2015 году. Теперь он закрыт. Где-то разработчик, который с тех пор дважды сменил карьеру, получает уведомление на GitHub.

```go
type Object struct{ name, color string }
type Point3D struct {
	Object
	x, y, z float64
}
type Line struct {
	Object
	p, q Point3D
}

// Go 1.27: используем продвинутое поле напрямую
line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
```

`name` не является полем `Line`. Оно является полем встроенного `Object`. Раньше нужно было писать `Line{Object: Object{name: "diagonal"}, ...}`.

Теперь о мелком шрифте, который я проверил, действительно попытавшись скомпилировать весь код.

**(a) Ключом по-прежнему является простой идентификатор.** Нельзя записать произвольный путь селектора.

```go
_ = Line{Object.name: "x"}   // invalid field name Object.name in struct literal
_ = Line{p.x: 1}             // invalid field name p.x in struct literal
```

Поэтому функция означает не «ключи теперь работают как доступ к полю», а именно «разрешены неявно продвинутые имена полей». Это несколько уже, чем подсказывает заголовок.

**(b) Нельзя одновременно указать встроенное поле и поле, продвинутое из него.**

```go
obj := Object{"edge", "black"}
_ = Line{Object: obj, name: "diagonal"}
// cannot specify promoted field name and enclosing embedded field Object
```

Разумно. Вы сообщили компилятору два противоречивых факта об одной и той же памяти, и он отказался угадывать.

**(c) Встраивание указателей не поддерживается.**

```go
type PtrEmbed struct {
	*Object
	z int
}

_ = PtrEmbed{name: "x"}
// invalid implicit pointer indirection to reach name
```

Чтобы пройти по указателю, нужен указатель, по которому можно пройти, а во время построения литерала его ещё нет. Это тоже справедливо.

Хорошая новость: `go fix` перепишет ваши старые литералы за вас. Подробнее об этом позже.

### 1.3. Вывод типов для функций стал менее произвольным

Вывод типов для обобщённых функций раньше работал в одних местах и не работал в других, без какого-либо очевидного принципа. Присваивание переменной? Работает. Помещение точно такой же функции в поле структуры? Ошибка компилятора: пожалуйста, напишите `double[int]`, как в 2022 году.

Go 1.27 делает вывод работающим в **каждом контексте, где целевой тип известен однозначно**:

```go
func double[T ~int | ~float64](v T) T { return v * 2 }

type S struct{ f func(int) int }
type A [1]func(int) int

func main() {
	s := S{f: double}          // 1.26: требовалось double[int]
	a := A{double}             // 1.26: требовалось double[int]

	c := make(chan func(int) int, 1)
	c <- double                // 1.26: требовалось double[int]

	var fn func(float64) float64 = double  // это всегда работало

	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
	// 42 10 14 3
}
```

Одна функция `double` инстанцируется как `func(int) int` в трёх местах и как `func(float64) float64` в четвёртом, полностью на основании контекста. Если вы пишете структуры опций или таблицы обработчиков, заполненные значениями функций, большое количество шума из `[T]` скоро исчезнет из вашей кодовой базы.

## 2. Среда выполнения: бесплатная производительность и детектор утечек

### 2.1. Аллокация, специализированная по размеру

Компилятор теперь генерирует вызовы подпрограмм аллокации, специализированных по размеру, для маленьких объектов. В примечаниях к выпуску заявлено до 30% выигрыша в аллокациях объектов размером менее 80 байт и около 1% в целом для программ, интенсивно выделяющих память.

Я не верю примечаниям к выпуску без измерений, поэтому:

```go
type small struct{ a, b, c, d int }

var sink any

func BenchmarkAlloc(b *testing.B) {
	for b.Loop() {
		sink = &small{}
	}
}

func BenchmarkSlice(b *testing.B) {
	for b.Loop() {
		sink = make([]byte, 48)
	}
}
```

```
# по умолчанию (size-specialized malloc включён)
BenchmarkAlloc-12     3000000     5.358 ns/op
BenchmarkSlice-12     3000000    13.58 ns/op

# GOEXPERIMENT=nosizespecializedmalloc
BenchmarkAlloc-12     3000000     8.722 ns/op
BenchmarkSlice-12     3000000    20.81 ns/op

```

На 30–35% быстрее в микробенчмарке, который ничего не делает, кроме аллокаций, на Apple Silicon. Иными словами: это лучший случай, достигнутый в лабораторных условиях бенчмарком, созданным для того, чтобы цифра выглядела хорошо. В реальной программе сборка мусора и реальная работа поглотят большую часть выигрыша. Утверждение о ~1% является честным.

Ценой является размер бинарного файла. Hello World:

```
2413202 bytes  (default)
2361826 bytes  (nosizespecializedmalloc)
```

Около 50 КБ, фиксированно, независимо от вашей программы. Можно отказаться от этого с помощью `GOEXPERIMENT=nosizespecializedmalloc`, но **этот запасной выход запланирован к удалению в Go 1.28**, поэтому воспринимайте его как обходной путь для отчёта об ошибке, а не как образ жизни.

### 2.2. Профиль утечек горутин теперь реален

Экспериментальный в Go 1.26, общедоступный в 1.27. GOEXPERIMENT `goroutineleakprofile` удалён; теперь всё работает само по себе.

```go
func leak() {
	ch := make(chan int) // никто никогда не отправит. никто.
	go func() {
		<-ch
	}()
}

func main() {
	for range 3 {
		leak()
	}
	runtime.GC()
	runtime.GC()
	pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
}
```

```
goroutineleak profile: total 3
3 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
#	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`.

```go
func handle(ctx context.Context) {
	var wg sync.WaitGroup
	wg.Go(func() {
		buf := make([]byte, 4<<10)
		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
	})
	wg.Wait()
}

func main() {
	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
	pprof.Do(context.Background(), labels, handle)
}
```

```
goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
main.handle.func1()
	/tmp/go127/tblabel/main.go:15 +0x40
sync.(*WaitGroup).Go.func1()
	...
goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
...
```

Важная деталь: **метки наследуются дочерними горутинами.** Горутина 3 никогда не устанавливала метку. Она получила её от своего родителя.

Поэтому в следующий раз, когда в продакшене случится дедлок и кто-то отправит процессу SIGQUIT, вместо 4000 горутин, которые все выглядят одинаково, вы получите 4000 горутин с пометкой о том, какому запросу и какому тенанту они принадлежат. Три строки `pprof.Do` в точке входа запроса дают вам это.

Поскольку метки могут содержать то, что вы не хотите выводить в stderr, `GODEBUG=tracebacklabels=0` отключает эту функцию, и этот вариант отказа явно предназначен для сохранения навсегда.

### 2.4. asynctimerchan окончательно удалён

Go 1.23 сделал канালы таймеров небуферизованными (синхронными) и предложил `asynctimerchan=1` как способ вернуться к старому поведению. В 1.27 эта настройка **окончательно удалена**. Каналы пакета `time` синхронны, точка.

Интересна политика, введённая вместе с этим. Удалённый GODEBUG, оставшийся в вашем `go.mod`, не ломает сборку автоматически; он ломает её только если установлено **старое** значение:

```
# go.mod содержит: godebug asynctimerchan=1
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)

# go.mod содержит: godebug asynctimerchan=0
(собирается нормально)
```

Это означает, что на вас будут ругаться только те, кто действительно полагался на удалённое поведение. Все, кто установил значение, которое в итоге стало значением по умолчанию, и забыл о нём, могут продолжать не думать об этом. Это продуманный образчик API-археологии.

## 3. Стандартная библиотека

### 3.1. encoding/json/v2: движок заменили на ходу

Это самое большое изменение. [Более двух лет обсуждения предложения](https://go.dev/issue/71497) наконец воплотились.

Теперь есть **три пакета**, и понимание их разделения составляет большую часть успеха:

| Пакет                    | Назначение                                                                     |
| ------------------------ | ------------------------------------------------------------------------------ |
| `encoding/json`          | API v1, который вы знаете. **Поведение на 100% не изменилось.** Теперь реализован поверх v2 |
| `encoding/json/v2`       | Семантическая обработка. Значения Go ↔ JSON                                    |
| `encoding/json/jsontext` | Синтаксическая обработка. JSON как поток токенов                               |

Главное: `encoding/json` был перестроен на совершенно другой реализации и **ведёт себя идентично**. Вот как:

```go
// encoding/json.Unmarshal в Go 1.27, сокращённо
func Unmarshal(data []byte, v any) error {
	return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
}
```

Каждая устаревшая особенность v1 закодирована как опция, объединена в `DefaultOptionsV1()`, и API v1 всегда применяет этот набор. Это означает, что следующий код ведёт себя одинаково в 1.26 и 1.27:

```go
var m map[string]int
json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)  // <nil>, map[a:2]
```

Да, v1 по-прежнему молча принимает повторяющиеся ключи и берёт последний. Так было всегда. Так есть и сейчас. Совместимость означает совместимость и с плохими частями.

**А v2 имеет собственное мнение:**

```go
var m map[string]int
jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
// jsontext: duplicate object member name "a"

var s string
jsonv2.Unmarshal([]byte("\"\xff\""), &s)
// jsontext: invalid UTF-8 after offset 1
```

Повторяющиеся ключи и некорректный UTF-8 отвергаются. Оба случая действительно опасны, а не просто неряшливы: когда два парсера расходятся во мнениях о том, какой повторяющийся ключ побеждает, возникают ошибки безопасности. CVE-2017-12635 в CouchDB был именно таким: тело JSON с двумя ключами `roles`, где валидатор читал один, а слой хранения другой. Отказ является правильным решением.

Опции передаются как variadic-параметры:

```go
type Config struct {
	Name    string   `json:"name"`
	Tags    []string `json:"tags,omitzero"`
	Timeout int      `json:"timeout,omitzero"`
}

out, _ := jsonv2.Marshal(Config{Name: "api"})
// {"name":"api"}

// сортировка ключей map и отступы
out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
	jsonv2.Deterministic(true), jsontext.WithIndent("  "))
// {
//   "a": 1,
//   "b": 2
// }

// отклонять неизвестные поля — раньше требовался Decoder и DisallowUnknownFields()
var c Config
err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
	jsonv2.RejectUnknownMembers(true))
// json: cannot unmarshal JSON string into Go main.Config:
//   unknown object member name "nope"
```

`Deterministic`, `MatchCaseInsensitiveNames`, `StringifyNumbers`, `FormatNilSliceAsNull`, `OmitZeroStructFields` — большинство вещей, которые раньше решались сторонней библиотекой или самописным `MarshalJSON`, теперь являются флагами.

А лучшая часть — история миграции. **Более поздние опции имеют приоритет**, поэтому вы можете внедрять строгость v2 по одному поведению за раз:

```go
// сохранить семантику v1, но отклонять повторяющиеся ключи, как это делает v2
jsonv2.Unmarshal(data, &v,
	json.DefaultOptionsV1(),
	jsontext.AllowDuplicateNames(false))
// duplicate object member name
```

Не нужно портировать кодовую базу в 200 тысяч строк на v2, чтобы перестать принимать повторяющиеся ключи. Вы переключаете одну опцию. Для крупных проектов это реалистичный путь.

Производительность: маршалинг примерно на уровне прежнего, **демаршалинг значительно быстрее.** Если что-то пойдёт не так, `GOEXPERIMENT=nojsonv2` восстанавливает старую реализацию, и этот вариант отказа тоже рано или поздно будет удалён, поэтому заводите issue, а не смиряйтесь.

`jsontext` — это низкоуровневый слой: `Encoder`/`Decoder`, которые обходят JSON как `Token` и `Value` с конечным автоматом, держащим вас в рамках. Обращайтесь к нему, когда пишете потоковый преобразователь или фильтр JSON и не хотите материализовывать значения Go вообще.

### 3.2. Стандартный пакет uuid

Наконец-то. RFC 9562 в стандартной библиотеке, без необходимости в `go get`.

```go
import "uuid"

func main() {
	fmt.Println(uuid.NewV4())
	// b97aa695-da08-472c-af81-ff088129019f

	// v7: старшие 48 бит — временная метка, поэтому они всегда сортируются в порядке создания
	a, b := uuid.NewV7(), uuid.NewV7()
	fmt.Println(a.Compare(b) < 0) // true

	u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
	fmt.Println(u, err)
	// 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>

	fmt.Println(uuid.Nil(), uuid.Max())
	// 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
}
```

Весь 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.

```go
sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
pk := sk.PublicKey()

msg := []byte("release the gophers")
opts := &mldsa.Options{Context: "gosuda.org/blog"}

sig, _ := sk.Sign(rand.Reader, msg, opts)

fmt.Println("private key seed:", len(sk.Bytes()), "bytes")  // 32
fmt.Println("public key:", len(pk.Bytes()), "bytes")        // 1952
fmt.Println("signature:", len(sig), "bytes")                // 3309

fmt.Println(mldsa.Verify(pk, msg, sig, opts))
// <nil>
fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
// 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`.

```go
// dst = a*x + y
func axpy(dst, x, y []float32, a float32) {
	va := simd.BroadcastFloat32s(a)
	w := va.Len()

	i := 0
	for ; i+w <= len(x); i += w {
		vx := simd.LoadFloat32s(x[i:])
		vy := simd.LoadFloat32s(y[i:])
		vx.MulAdd(va, vy).Store(dst[i:])
	}
	// обрабатываем хвост частичной загрузкой/записью — без скалярного цикла очистки
	if i < len(x) {
		vx, _ := simd.LoadFloat32sPart(x[i:])
		vy, _ := simd.LoadFloat32sPart(y[i:])
		vx.MulAdd(va, vy).StorePart(dst[i:])
	}
}
```

```bash
$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
vector bits: 128 emulated: false
[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

Новый интерфейс, описывающий контракт между типом значения и хеш-контейнерами:

```go
type Hasher[T any] interface {
	Hash(*Hash, T)
	Equal(x, y T) bool
}
```

Зачем? Встроенный map Go принимает только ключи `comparable`. Нельзя использовать слайс в качестве ключа. Нельзя определить «равенство без учёта регистра». `Hasher` решает обе задачи одновременно:

```go
type CaseInsensitive struct{}

func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
	h.WriteString(strings.ToLower(s))
}
func (CaseInsensitive) Equal(x, y string) bool {
	return strings.ToLower(x) == strings.ToLower(y)
}

var seed = maphash.MakeSeed()

func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
	var h maphash.Hash
	h.SetSeed(seed)   // тот же seed, иначе всё это ничего не значит
	hr.Hash(&h, v)
	return h.Sum64()
}

fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
// true
```

Для обычной семантики `==` существует `ComparableHasher[T]`:

```go
hashOf(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`) завершает картину.

Вот тест повторных попыток с экспоненциальной задержкой:

```go
func TestRetryWithBackoff(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		var hits int
		srv := httptest.NewTestServer(t, http.HandlerFunc(
			func(w http.ResponseWriter, r *http.Request) {
				hits++
				if hits < 3 {
					w.WriteHeader(http.StatusServiceUnavailable)
					return
				}
				io.WriteString(w, "ok")
			}))

		client := srv.Client()
		start := time.Now()
		var body string
		for attempt := range 5 {
			resp, err := client.Get(srv.URL)
			if err != nil {
				t.Fatal(err)
			}
			b, _ := io.ReadAll(resp.Body)
			resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				body = string(b)
				break
			}
			synctest.Sleep(time.Duration(1<<attempt) * time.Second)
		}

		if body != "ok" {
			t.Fatalf("got %q", body)
		}
		// задержка должна быть ровно 1с + 2с = 3с
		if elapsed := time.Since(start); elapsed != 3*time.Second {
			t.Fatalf("elapsed = %v, want 3s", elapsed)
		}
		t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
	})
}
```

```
=== RUN   TestRetryWithBackoff
    x_test.go:48: hits=3 elapsed=3s
--- 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 года:

```go
// больше не нужно
defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()
```

Для большинства программ это ничего не меняет или даёт небольшой выигрыш. Если же становится _хуже_, вы, вероятно, попали в категорию, которую примечания к выпуску вежливо описывают: `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% из нас с первого раза допускали ошибку на единицу.

```go
name, ext, ok := strings.CutLast("archive.tar.gz", ".")
// archive.tar gz true
```

Отлично подходит для расширений файлов и для разбора `host:port`, где нужно найти _последнее_ двоеточие, потому что в адресах IPv6 их много.

**`math/big.Int.Divide`.** Деление с явным режимом округления, заменяющее вопрос «мне нужен `Quo` или `Div`?» подбрасыванием монетки:

```go
x, y := big.NewInt(-7), big.NewInt(2)

new(big.Int).Divide(x, y, new(big.Int), big.Trunc)  // q=-3 r=-1
new(big.Int).Divide(x, y, new(big.Int), big.Floor)  // q=-4 r=1
new(big.Int).Divide(x, y, new(big.Int), big.Round)  // q=-4 r=1
new(big.Int).Divide(x, y, new(big.Int), big.Ceil)   // q=-3 r=-1
```

Если вы работаете в предметной области, где правило округления записано в нормативном документе, это для вас.

**`url.URL.Clone` и `url.Values.Clone`.** Глубокие копии. Старое поверхностное копирование `*u` разделяло указатель `Userinfo`, что порождало ровно тот тип ошибок, который ищут день, а исправляют за пять секунд.

```go
orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
clone := orig.Clone()
clone.Host = "example.com"
fmt.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` для предпросмотра:

```go
package fixdemo

import (
	"sync"
	"sync/atomic"
)

type Base struct{ ID int }
type User struct {
	Base
	Name string
}

func New() User {
	return User{Base: Base{ID: 1}, Name: "gopher"}
}

var counter int64

func Incr() { atomic.AddInt64(&counter, 1) }

func Reverse(s []string) {
	for i := len(s) - 1; i >= 0; i-- {
		_ = s[i]
	}
}

func Spawn(wg *sync.WaitGroup) {
	wg.Add(1)
	go func() {
		defer wg.Done()
	}()
}
```

```bash
$ go1.27rc3 fix -diff ./fixdemo
```

```diff
 import (
+	"slices"
 	"sync"
 	"sync/atomic"
 )

 func New() User {
-	return User{Base: Base{ID: 1}, Name: "gopher"}
+	return User{ID: 1, Name: "gopher"}
 }

-var counter int64
+var counter atomic.Int64

-func Incr() { atomic.AddInt64(&counter, 1) }
+func Incr() { counter.Add(1) }

 func Reverse(s []string) {
-	for i := len(s) - 1; i >= 0; i-- {
-		_ = s[i]
+	for _, v := range slices.Backward(s) {
+		_ = v
 	}
 }

 func Spawn(wg *sync.WaitGroup) {
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-	}()
+	wg.Go(func() {
+	})
 }
```

Четыре модернизатора сработали на одном небольшом файле:

- `embedlit` — переписывает встроенные литералы, используя новый синтаксис из §1.2
- `atomictypes` — `atomic.AddInt64(&x, 1)` превращается в `atomic.Int64.Add(1)`, что делает неатомарный доступ невозможным и тихо исправляет ошибки выравнивания для 32-битных платформ, о которых вы не подозревали
- `slicesbackward` — обратные циклы превращаются в `slices.Backward`
- `waitgroupgo` — ритуал `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` из vet, отмечая символы стандартной библиотеки, которые новее, чем позволяет директива `go` в вашем `go.mod`:

```go
// go.mod содержит: go 1.24
package sv

import "strings"

func Ext(name string) string {
	_, ext, _ := strings.CutLast(name, ".")  // CutLast — символ 1.27
	return ext
}
```

```
$ go1.27rc3 test ./...
# sv
./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
FAIL	sv [build failed]
```

Это устраняет классический сценарий сбоя, когда всё работает на вашей машине (с последним инструментарием), а в CI или в окружении пользователя со старой версией всё падает. Если вы публикуете библиотеки с консервативной директивой `go`, это подарок.

### 4.3. go test -json получил OutputType

Строки `"Action":"output"` теперь содержат необязательное поле `"OutputType"`: `"error"`, `"error-continue"` или `"frame"`.

```
'frame'  '=== RUN   TestFail\n'
None     '    x_test.go:6: hello\n'
'error'  '    x_test.go:7: boom\n'
'frame'  '--- FAIL: TestFail (0.00s)\n'
'frame'  'FAIL\n'
```

Вывод `t.Log` (без поля), вывод `t.Error` (`error`) и строки, сгенерированные фреймворком (`frame`), теперь различимы. Всем, кто писал регулярное выражение, чтобы понять, какие строки вывода теста являются «настоящими», теперь его можно удалить.

### 4.4. Улучшения go doc

**Синтаксис `package@version`.** Читайте документацию для конкретной версии, не добавляя её в свой модуль:

```bash
$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
package errgroup // import "golang.org/x/sync/errgroup"
...
```

Удобно для проверки изменений перед обновлением.

**`-ex` и вывод исходного кода примеров:**

```bash
$ go1.27rc3 doc -ex strings | grep Example
    func ExampleClone()
    func ExampleCompare()
    func ExampleContains()
    func ExampleCut()
    func ExampleCutPrefix()
    ...

$ go1.27rc3 doc strings.ExampleCut
package main

import (
	"fmt"
	"strings"
)

func main() {
	show := func(s, sep string) {
		before, after, found := strings.Cut(s, sep)
		fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
	}
	show("Gopher", "Go")
	...
}

Output:
Cut("Gopher", "Go") = "", "pher", true
```

Исходный код примера _и_ ожидаемый вывод прямо в терминале, без открытия вкладки браузера, которая всё ещё будет открыта в следующий четверг.

### 4.5. go mod tidy наводит порядок в блоках require

Для модулей с `go 1.27` или новее `go mod tidy` объединяет разрозненные блоки `require` максимум в два (прямые и косвенные), сохраняя прикреплённые комментарии.

```go
// До
module tidydemo

go 1.27

require golang.org/x/sync v0.10.0

// networking
require golang.org/x/net v0.33.0

require (
	golang.org/x/sys v0.28.0 // indirect
)

require golang.org/x/text v0.21.0 // indirect
```

```go
// После: go mod tidy
module tidydemo

go 1.27

require (
	// networking
	golang.org/x/net v0.33.0
	golang.org/x/sync v0.10.0
)
```

Обратите внимание, что комментарий `// 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 десятилетие латал одной и той же сторонней зависимостью.

Если хотите порядок приоритетов, чтобы действительно получить пользу от этого:

1. **`httptest.NewTestServer` + `synctest.Sleep`** — зависящие от времени HTTP-тесты становятся быстрыми и детерминированными. Лучшее соотношение усилий и отдачи во всём релизе.
2. **Профиль утечек горутин** — откройте один эндпоинт в staging и приготовьтесь к смирению.
3. **Метки горутин в трассировках** — три строки `pprof.Do` в точке входа запроса превращают ваш следующий дамп стека в 3 часа ночи во что-то читаемое.
4. **`go fix ./...`** — пусть инструмент займётся модернизацией.
5. **`encoding/json/v2`** — без спешки. Внедряйте по одной опции за раз.

Возьмите `go1.27rc3` и прогоните свой тестовый набор прямо сейчас. Всё из этого контрольного списка гораздо приятнее обнаружить во вторник после обеда, чем во время инцидента.

**Ссылки**

- [Go 1.27 Release Notes](https://go.dev/doc/go1.27)
- [The Go Programming Language Specification](https://go.dev/ref/spec)
- [proposal: encoding/json/v2 (#71497)](https://go.dev/issue/71497)
- [proposal: portable SIMD (#78902)](https://go.dev/issue/78902)
- [proposal: hash/maphash.Hasher (#70471)](https://go.dev/issue/70471)
