GoSuda

Go 1.27 или как се научих да спра да се тревожа и да заобичам Generic Methods

By iwanhae
views ...

Преглед на Go 1.27

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

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

1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download

1. Езикът се промени (Да, наистина)

1.1. Генерични методи

От Go 1.18 разполагаме с генерици и оттогава водим „Разговора“. Той звучи така:

„Нека просто напиша метод Map за моя тип slice —“

method must have no type parameters

„...добре. Нека бъде функция на ниво пакет.“

Методите можеха да използват само типови параметри, декларирани от приемника (receiver). Вашият собствен метод не можеше да въвежда нови такива. Така всяка генерична трансформация беше изпратена в обхвата на пакета, където стоеше до седемнадесет други помощни функции с вариации на името MapSlice.

Go 1.27 коригира това:

 1type List[E any] []E
 2
 3// F е типов параметър, деклариран от самия метод
 4func (l List[E]) Apply[F any](f func(E) F) List[F] {
 5	r := make(List[F], len(l))
 6	for i, x := range l {
 7		r[i] = f(x)
 8	}
 9	return r
10}
11
12func main() {
13	l := List[int]{1, 2, 3}
14	fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
15	// [10 20 30]
16}

Приемникът дори не е задължително да бъде генеричен. Обикновена структура може да има генеричен метод:

1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }

Сега, преди да рефакторирате целия си кодов проект този следобед, има две ограничения и те са по-значими, отколкото изглеждат:

  1. Методите на интерфейси не могат да декларират типови параметри.
  2. Генеричните методи не могат да реализират методи на интерфейси.

Второто ограничение е това, което ще ви създаде проблеми:

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

Следователно генеричните методи и динамичното изпращане (dynamic dispatch) не работят заедно. Това не е защото екипът на 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. Селектори на полета в литерали на структури или: Проблем #9859 най-накрая е решен

Вграждането (embedding) ви дава u.ID. Вграждането не ви дава User{ID: 1}. Вместо това ви дава User{Base: Base{ID: 1}}, което е начинът на Go да попита дали наистина сте имали предвид това.

Проблем #9859 беше подаден през 2015 г. Сега той е затворен. Някъде там, един gopher, който е сменил професията си два пъти оттогава, получава известие в GitHub.

 1type Object struct{ name, color string }
 2type Point3D struct {
 3	Object
 4	x, y, z float64
 5}
 6type Line struct {
 7	Object
 8	p, q Point3D
 9}
10
11// Go 1.27: използвайте директно промотираното поле
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name не е поле на Line. То е поле на вградения Object. Преди трябваше да пишете Line{Object: Object{name: "diagonal"}, ...}.

Сега за подробностите, които проверих, опитвайки се да компилирам всичко това.

(а) Ключът все още е обикновен идентификатор. Не можете да напишете произволен път за селектор.

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

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

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

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

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

(в) Вграждането на указатели не е включено.

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

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# default (size-specialized malloc on)
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. Това е най-добрият случай, постигнат в лабораторни условия. В реална програма Garbage Collector-ът и същинската работа ще смекчат този резултат. Твърдението за ~1% е по-честно.

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

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

Около 50KB, фиксирано, независимо от програмата ви. Можете да се откажете с GOEXPERIMENT=nosizespecializedmalloc, но тази вратичка е планирана за премахване в Go 1.28, така че я третирайте като решение за докладване на грешки, а не като начин на живот.

2.2. Профилът за течове на Goroutine вече е реален

Експериментален в 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

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

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

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

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

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

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

2.3. Tracebacks вече ви казват коя заявка е умряла

За модули, деклариращи Go 1.27 или по-нова версия, заглавните редове на traceback вече включват етикети (labels) на горутини от runtime/pprof.

 1func handle(ctx context.Context) {
 2	var wg sync.WaitGroup
 3	wg.Go(func() {
 4		buf := make([]byte, 4<<10)
 5		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
 6	})
 7	wg.Wait()
 8}
 9
10func main() {
11	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12	pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3	/tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5	...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...

Важната подробност: етикетите се наследяват от дъщерните горутини. Горутина 3 никога не е задавала етикет. Тя получи такъв от своя родител.

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

Тъй като етикетите могат да съдържат неща, които бихте предпочели да не извеждате в stderr, GODEBUG=tracebacklabels=0 ги изключва и това е предвидено да остане завинаги.

2.4. asynctimerchan е премахнат завинаги

Go 1.23 направи каналите на таймера небуферирани (синхронни) и предложи asynctimerchan=1 като начин за връщане към старото поведение. В 1.27 тази настройка е трайно премахната. Каналите на пакета time са синхронни, точка, без преговори.

Интересната част е политиката, въведена заедно с това. Премахнат GODEBUG, оставен във вашия go.mod, не чупи автоматично build-а — той се чупи само ако е зададен на старата стойност:

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(build-ът преминава успешно)

Което означава, че единствените хора, на които ще се „вика“, са тези, които действително са разчитали на премахнатото поведение. Всеки, който го е задал на евентуалното подразбиране и е забравил за него, може да продължи да не мисли за това. Това е внимателно парче API археология.

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

3.1. encoding/json/v2: Замениха механизма по време на полет

Това е голямото събитие. Повече от две години обсъждане на предложението най-накрая приключи.

Сега има три пакета и разбирането на разделението е по-голямата част от битката:

ПакетЗадача
encoding/jsonv1 API, което познавате. Поведението е 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() и v1 API винаги прилага този пакет. Което означава, че това се държи по един и същ начин в 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, където валидаторът е чел единия, а слоят за съхранение — другия. Отхвърлянето е правилното решение.

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

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

jsontext е ниско ниво: Encoder/Decoder, обхождащи JSON като Tokenи и Valueта със състояние (state machine), което ви държи отговорни. Използвайте го, когато пишете стрийминг трансформатор или 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-tree индекс спира да се фрагментира, в което v4 UUID са известни като лоши.

Вече можете да изтриете зависимост. Ако имате нужда от 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-кратно увеличение, а публичният ключ е почти 2KB отгоре. Сложете няколко от тях в сертификатна верига и вашият TLS ръкостискане започва да се нуждае от собствена MTU стратегия.

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

Options.Context е за отделяне на домейни: подписвайте с един и същ ключ за различни цели, използвайте различен контекст за всяка и подпис от един контекст няма да бъде валиден в друг. Примерът по-горе показва точно това — същият ключ, същото съобщение, различен контекст, отхвърлено.

PrivateKey реализира crypto.Signer, така че се вписва в съществуващите интерфейси. crypto/x509 обработва ML-DSA ключове и подписи, а crypto/tls поддържа схемите за подпис MLDSA44/MLDSA65/MLDSA87 в TLS 1.3.

Има и SignDeterministic, който пропуска случайността — удобно за тестове и възпроизводими build-ове.

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 ключове. Не можете да използвате slice като ключ. Не можете да дефинирате „равно без значение от регистъра“. 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 е интерфейс, а не структура от данни. Все още няма стандартна хеш таблица или Bloom филтър, който да го консумира. Това е основа за бъдещ пакет с контейнери. Днес бихте го използвали, когато строите собствена структура или консумирате съществуваща реализация като go/types.Hasher (който ви позволява да използвате types.Type като ключ в map, спазвайки Identical).

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

3.6. httptest.NewTestServer + synctest.Sleep: Големият хит

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

testing/synctest завърши в Go 1.25 и ни даде фалшив часовник за тестване на конкурентен код. Само че в момента, в който тестът ви докосне истинска мрежа, илюзията се срива. httptest.NewTestServer в Go 1.27 използва фалшива мрежа в паметта, така че балонът остава непокътнат. А synctest.Sleep (= time.Sleep + synctest.Wait) го допълва.

Ето тест за повторен опит с експоненциално отстъпление (retry-with-exponential-backoff):

 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		// backoff трябва да бъде точно 1s + 2s = 3s
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 спи и след това изчаква, докато всяка друга горутина в балона е трайно блокирана, така че наблюдавате системата, след като се е успокоила.

Таймаути, повторни опити, прекъсвачи (circuit breakers), ограничители на скоростта — всеки тест на 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. Ограничава колко стойности на заглавия (header values) ще приеме сървърът, по подразбиране DefaultMaxHeaderValueCount. Още една врата затворена за „изпрати десет хиляди заглавия и гледай какво ще стане“.

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

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.2
  • atomictypesatomic.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 по подразбиране, маркирайки символи от стандартната библиотека, които са по-нови, отколкото директивата 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]

Това убива класическия режим на провал, при който всичко работи на вашата машина (най-новия toolchain) и експлодира в 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) и редовете, генерирани от framework-а (frame), сега са различими. Всеки, който е писал regex, за да разбере кои редове от изхода на теста са „истински“, сега може да го изтрие.

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 merge, където се раждат заблудени require блокове. Ако вашият екип има повече от трима души, добавящи зависимости, конфликтите във вашия go.mod скоро ще станат забележимо по-редки.

4.6. Разни

  • Поддръжката за bzr е премахната. Ако това ви засяга, искрено бих искал да чуя историята.
  • go tool trace -http се свързва към localhost, когато му е даден само порт. -http=:6060 вече не слуша на всеки интерфейс. Използвайте -http=0.0.0.0:6060, ако сте имали предвид това. Сега съвпада с go tool pprof и предотвратява случайния публичен profiler.
  • Файлове с отговори (@file) се поддържат от compile, link, asm, cgo, cover и pack във формат, съвместим с GCC. За build системи, които надвишават лимитите на дължината на командния ред — здравей, Bazel.

5. Компилатор, Linker, Портове

Компилатор. Относителните имена на файлове в директиви //line сега се разрешават спрямо директорията на съдържащия ги файл, съвпадайки с go/scanner. Уместно, ако пишете генератори на код.

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

Linker. Нови опции -macos и -macsdk задават версиите на OS и SDK в командата за зареждане LC_BUILD_VERSION на macOS.

Портове.

  • Darwin сега изисква macOS 13 Ventura или по-нова, както беше обявено в 1.26. Проверете вашите CI runners.
  • PowerPC (GOOS=linux GOARCH=ppc64) премина към ELFv2 ABI. Изисква Linux kernel 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 — те чупят build-а само ако са зададени на старите си стойности.
  • Нарушения на stdversion. go test ги улавя сега. Изпълнете го рано, ако поддържате библиотека с консервативна директива go.
  • CI runners с 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. Профилът за течове на горутини — изложете един endpoint в staging и се подгответе да бъдете смирени.
  3. Traceback етикети на горутини — три реда pprof.Do във входната точка на вашата заявка превръщат следващия ви stack dump в 3 сутринта в нещо четимо.
  4. go fix ./... — оставете инструмента да извърши модернизацията.
  5. encoding/json/v2 — няма бързане. Приемайте го опция по опция.

Вземете go1.27rc3 и изпълнете вашия набор от тестове срещу него сега. Всяко нещо от този списък е значително по-приятно да бъде открито във вторник следобед, отколкото по време на инцидент.

Референции