---
id: 1a31e06c0d969b4b6cbbc590259cf089
author: iwanhae
title: Go 1.27, или как се научих да престана да се тревожа и да обичам обобщените методи
description: Generic methods, encoding/json/v2, стандартен пакет uuid и детектор за течове на goroutine, който наистина работи. Обзор на Go 1.27 с примери, всички от които са действително изпълнени.
language: bg
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
---

![Преглед на Go 1.27](/assets/images/whats-new-in-go1.27/overview.webp)

Go 1.27 излиза през август 2026 г., и този път самият език се промени. Не в смисъл на „добавихме нова функция към `slices`“, а наистина. Генеричните методи вече са налични. Един единайсетгодишен проблем беше затворен. `encoding/json` беше тихо заменен с нов engine, докато самолетът беше във въздуха.

Всеки пример по-долу беше изпълнен на `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]
}
```

Receiver-ът дори не трябва да е генеричен. Обикновена структура може да има генеричен метод:

```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 г. Сега е затворен. Някъде един gopher, който оттогава е сменил кариерата си два пъти, получава известие от 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
```

Така че функцията не означава „ключовете работят като достъп до полета сега“. Тя означава конкретно „неявно повишените имена на полета са разрешени“. Малко по-тясно от заглавието.

**(б) Не можеш едновременно да укажеш вградено поле и нещо, повишено от него.**

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

Разумно. Казал си две противоречиви неща за една и съща памет и компилаторът отказа да гадае.

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

```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. Runtime: безплатна производителност и детектор за течове

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

Компилаторът вече генерира извиквания към алокационни рутини, специализирани по размер, за малки обекти. Release notes твърдят до 30% по-малко време за алокации под 80 байта и около 1% общо за програми с интензивни алокации.

Не вярвам на release notes без измерване, затова:

```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. Иначе казано: това е най-добрият случай, постигнат при лабораторни условия от бенчмарк, проектиран така, че числото да изглежда добре. В реална програма GC и действителната работа ще погребат по-голямата част от това. Твърдението за ~1% е честното.

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

```
2413202 bytes  (по подразбиране)
2361826 bytes  (nosizespecializedmalloc)
```

Около 50KB, фиксирано, независимо от програмата ти. Можеш да се откажеш с `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
```

Три горутини, завинаги блокирани, с файл и номер на ред, които сочат точно мястото, където си го направил.

Трикът зад това е наистина умен: той **използва повторно анализа на достижимостта на garbage collector-а**. Ако горутина G е блокирана на примитив P и P е недостижим от каквато и да е изпълнима горутина (или от нещо, което тези горутини биха могли да събудят), тогава никой не може никога да докосне P отново, следователно G никога няма да се събуди. Това не е евристика, а доказателство.

Същият дизайн ти дава и ограничението безплатно: ако каналът или мютексът са достижими през глобална променлива или през локална променлива на някоя още работеща горутина, GC все още ги вижда и runtime не може да заключи нищо. Забележи, че дори малкият пример по-горе се нуждае от две извиквания на `runtime.GC()`, за да докладва. Няма да хване всичко.

Ще хване обаче класическото „забравих да отменя контекста, горутината наworker-а живее вечно“, което е най-честият теч през повечето време.

Ако импортираш `net/http/pprof`, той е достъпен и на `/debug/pprof/goroutineleak`. Включи го в staging, проверявай го всяка седмица и бъди тихо ужасен.

Тази работа е допринесена от Vlad Saioc в Uber, който заслужава напитка по свой избор.

### 2.3. Traceback-овете вече казват кой заявка е умряла

За модули, декларирани с Go 1.27 или по-нова версия, редовете със заглавие на traceback сега включват етикетите на горутините от `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 никога не е задавала етикет. Тя е получила такъв от родителя си.

Затова следващият път, когато продукцията се 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`, не чупи автоматично билда; той го чупи само ако е зададен на **старата** стойност:

```
# 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
(builds fine)
```

Това означава, че единствените хора, на които се карат, са тези, които действително са разчитали на премахнатото поведение. Всеки, който я е задал на бъдещото по подразбиране и я е забравил, може да продължи да не мисли за нея. Това е разумно парче 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`, където валидаторът прочете единия, а слоят за съхранение прочете другия. Отхвърлянето е правилното решение.

Опциите са вариадични:

```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 000 реда към v2, за да спреш да приемаш дублирани ключове. Превключваш една опция. За всичко голямо това е реалистичният път.

Производителност: маршалирането е приблизително на нивото на предишното, **демаршалирането е значително по-бързо.** Ако нещо се обърка, `GOEXPERIMENT=nojsonv2` възстановява старата имплементация, а този изход за спасение също ще бъде премахнат в някакъв момент, така че подай issue, вместо да се установяваш.

`jsontext` е нискоуровневият слой: `Encoder`/`Decoder`, които обхождат JSON като `Token` и `Value` със state machine, която те държи коректен. Посегни към него, когато пишеш поточен трансформатор или 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-кратно увеличение, а публичният ключ е почти 2KB отгоре. Наблъскай няколко от тях във верига от сертификати и TLS handshake-ът ти ще започне да се нуждае от собствена 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` е интерфейс, а не структура от данни.** Все още няма стандартна хеш таблица или Bloom филтър, които да го консумират. Това е основа за бъдещ пакет с контейнери. Днес би го използвал, когато строиш собствена структура, или би консумирал съществуваща имплементация като `go/types.Hasher` (която ти позволява да използваш `types.Type` като ключ на map, съобразявайки се с `Identical`).

Управлението на seed е във вашите ръце. Ако `hashOf` по-горе създаваше нов seed при всяко извикване, еднаквите стойности щяха да се хешират различно и примерът щеше да отпечата `false` — точно бъгът, който написах при първия си опит. Един seed на контейнер. (Seed-ът е рандомизиран, за да се защити от DoS атаки чрез наводняване на хеш таблицата, затова не е просто константа.)

### 3.6. httptest.NewTestServer + synctest.Sleep: Удар по Sleeper-а

Ако възприемеш само едно нещо от това издание, нека бъде това.

`testing/synctest` излезе от експеримент в Go 1.25 и ни даде фалшив часовник за тестване на конкурентен код. Но в момента, в който тестът докоснеше реална мрежа, илюзията се разпадаше. `httptest.NewTestServer` в Go 1.27 използва **мрежа в паметта**, така че балонът остава непокътнат. А `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)
		}
		// backoff трябва да е точно 1s + 2s = 3s
		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)
```

Прочети това два пъти. Симулира три секунди backoff срещу реален HTTP сървър и приключи за **0.00 секунди**. А проверката е `elapsed == 3*time.Second` — _точно_ три секунди, а не „поне три секунди, плюс-минус джитър на планировчика“. Фалшивите часовници не се джитърят.

`synctest.Sleep` съществува по конкретна причина: ако тестът ти спи за същата продължителност като тествания код, кой ще се събуди първи е загадка. `synctest.Sleep` спи _и след това_ чака, докато всяка друга горутина в балона не бъде трайно блокирана, така че наблюдаваш системата след като се е уталожила.

Таймаутите, повторните опити, circuit breaker-ите, rate limiter-ите — всеки тест на 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()
}()
```

За повечето програми това е без ефект или малка печалба. Ако нещо се влошава, вероятно си в категорията, която release notes деликатно описват: `Transport.MaxIdleConns`, зададен на 0, или нов `Client` за всяка заявка, които заобикалят напълно лимита на неактивните връзки. `Transport.DisableKeepAlives = true` ще го скрие, но действителният съвет в release notes е, че „по-дълбок поглед вероятно би бил полезен“, което на езика на екипа на Go означава _имаш по-големи проблеми_.

**Приоритет на клиента в HTTP/2 (RFC 9218).** Сървърът вече зачита сигналите за приоритет от клиента. Ако предпочиташ старото round-robin планиране, `Server.DisableClientPriority = true`.

**`Server.MaxHeaderValueCount`.** Ограничава колко стойности на заглавни полета сървърът ще приеме, по подразбиране `DefaultMaxHeaderValueCount`. Още една врата, затворена за „изпрати десет хиляди заглавия и виж какво се случва“.

**ALPN при потребителски предоставени връзки.** Ако твоят `net.Conn` имплементира `ConnectionState() tls.ConnectionState`, `Transport` и `Server` ще извършат TLS ALPN договаряне върху него — така че персонализираните dialer-и, минаващи през прокси, пак могат да договорят 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` беше премахнат „поради стилистични съображения“, което е красиво дипломатичен начин да се опише случилото се в тази нишка на issue.

Пускането на `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]
```

Това слага край на класическия провал, при който всичко работи на твоята машина (най-новият toolchain) и се взривява в 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 merge**, където се раждат разпилените блокове `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`. Важно, ако пишеш генератори на код.

Имената на символи за функционални литерали (closures) също са по-прости — същото име независимо от инлайнването, а няколко инстанции на един и същ литерал може да споделят код в бинарния файл. Без функционална промяна, _освен_: код, който сравнява идентичността на функции чрез `reflect.Value.Pointer`, ще вижда „равно“ по-често от преди. Това сравнение никога не е било валидно, но ако го имаш, сега е моментът, в който ще те лъже по-силно.

**Линкер.** Новите опции `-macos` и `-macsdk` задават версиите на ОС и SDK в командата за зареждане `LC_BUILD_VERSION` на macOS.

**Платформи.**

- **Darwin вече изисква macOS 13 Ventura или по-нова**, както беше обявено в 1.26. Провери CI runner-ите си.
- **PowerPC (`GOOS=linux GOARCH=ppc64`) премина към ABI ELFv2.** Изисква Linux ядро 3.13+ (RHEL7 с backport към 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 runner-и с macOS 12 или по-стар.** Поддръжката е прекратена.
- [ ] **Тестове, които проверяват имената на символите на функционални литерали**, и **сравнения на функции чрез `reflect.Value.Pointer`.**
- [ ] **`Transport.MaxIdleConns = 0`** или `Client` за всяка заявка. Заедно с автоматичното изпразване на телата на отговорите това може да забави нещата.

## Заключителни мисли

Go 1.27 е изданието, в което куп отложена поддръжка дойде на кредит наведнъж.

Генеричните методи бяха липсващото парче от работата по генериците от 1.18. Селекторите на полета в литерали на структури затвориха единайсетгодишен issue. `encoding/json/v2` беше две години обсъждане на предложение. А `uuid` запълни дупка, която почти всеки Go проект беше кръпкал със същата външна зависимост в продължение на десетилетие.

Ако искаш приоритетен ред за реално извличане на полза от това:

1. **`httptest.NewTestServer` + `synctest.Sleep`** — тестовете на HTTP, зависими от времето, стават бързи и детерминистични. Най-добро съотношение усилие към полза в цялото издание.
2. **Профилът за течове на горутини** — изложи един endpoint в staging и се подгответе да се смирите.
3. **Етикети на горутините в traceback-овете** — три реда `pprof.Do` във входната точка на заявките превръщат следващия ти stack dump в 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)
