Go 1.27, czyli jak przestałem się martwić i pokochałem metody generyczne


Go 1.27 pojawia się w sierpniu 2026 roku i tym razem sam język się zmienił. Nie chodzi o "dodaliśmy nową funkcję do slices", lecz o rzeczywistą zmianę. Metody generyczne zostały wprowadzone. Jedenastoletnie zgłoszenie zostało zamknięte. encoding/json zostało po cichu zastąpione nowym silnikiem, gdy samolot był w powietrzu.
Każdy poniższy przykład został uruchomiony na go1.27rc3 (darwin/arm64). Każdy blok wyjścia jest prawdziwym wynikiem, skopiowanym dosłownie, w tym komunikaty o błędach. Jeśli coś wygląda dziwnie, to dlatego, że tak właśnie odpowiedział kompilator.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Język się zmienił (tak, naprawdę)
1.1. Metody generyczne
Od Go 1.18 mamy generyki, a od Go 1.18 mamy też Rozmowę. Przebiega ona tak:
"Napiszę po prostu metodę
Mapna moim typie slice'a..."
method must have no type parameters"...no dobrze. Niech będzie funkcja na poziomie pakietu."
Metody mogły używać wyłącznie parametrów typów zadeklarowanych przez receiver. Własna metoda nie mogła wprowadzić nowych. Dlatego każda generyczna transformacja była zsyłana do zakresu pakietu, gdzie siedziała obok siedemnastu innych luźnych pomocników o nazwach będących odmianami MapSlice.
Go 1.27 to naprawia:
1type List[E any] []E
2
3// F to parametr typu zadeklarowany przez samą metodę
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}
Receiver nie musi być nawet generyczny. Zwykła struktura może mieć metodę generyczną:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Zanim jednak zaczniesz dziś po południu refaktoryzować całą bazę kodu, istnieją dwa ograniczenia, które mają większe znaczenie, niż może się wydawać:
- Metody interfejsów nie mogą deklarować parametrów typów.
- Metody generyczne nie mogą implementować metod interfejsów.
To drugie jest tym, które將 Cię ugryzie:
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
Metody generyczne i dynamiczne wywołania (dispatch) więc się nie łączą. To nie skąpstwo zespołu Go: metoda generyczna ma nieskończenie wiele instancjacji, a tabela metod interfejsu musi być skończona i znana w czasie kompilacji. Nie da się umieścić czegoś nieskończonego w skończonej tabeli. Wszechświat powiedział nie.
Praktyczny wniosek: metody generyczne służą konkretnym typom o API w stylu narzędziowym. Wszystko, co ma się ukrywać za interfejsem, nadal wymaga starego podejścia.
Biblioteka standardowa już z tego skorzystała. Rand z math/rand/v2 dostał metodę generyczną:
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
Wcześniej generyczna była tylko funkcja pakietowa rand.N, co oznaczało korzystanie z globalnego źródła. Teraz Twój własny, zasiewany *Rand dostaje tę samą wygodę. Drobiazg. Ale miły.
1.2. Selektory pól w literałach struktur, czyli: zgłoszenie #9859 wreszcie zostaje zamknięte
Osadzanie daje u.ID. Osadzanie nie daje User{ID: 1}. Zamiast tego daje User{Base: Base{ID: 1}}, co jest sposobem Go na upewnienie się, czy naprawdę o to chodziło.
Zgłoszenie #9859 zostało złożone w 2015 roku. Teraz zostało zamknięte. Gdzieś gopher, który od tamtej pory dwukrotnie zmienił karierę, dostaje powiadomienie z GitHuba.
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: użycie promowanego pola bezpośrednio
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name nie jest polem Line. Jest polem osadzonego Object. Wcześniej trzeba było napisać Line{Object: Object{name: "diagonal"}, ...}.
Teraz drobny druk, który sprawdziłem, faktycznie próbując skompilować wszystko.
(a) Kluczem nadal jest zwykły identyfikator. Nie można zapisać dowolnej ścieżki selektora.
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
Zatem ta funkcja nie oznacza, że "klucze działają teraz jak dostęp do pól". Oznacza konkretnie, że dozwolone są domyślnie promowane nazwy pól. Jest to nieco węższe, niż sugeruje nagłówek.
(b) Nie można jednocześnie podać osadzonego pola i czegoś promowanego z niego.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Rozsądne. Podałeś dwie sprzeczne informacje o tej samej pamięci, a kompilator odmówił zgadywania.
(c) Osadzanie wskaźników nie jest mile widziane.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Aby podążać za wskaźnikiem, potrzebujesz wskaźnika, za którym można podążać, a w momencie konstruowania literału jeszcze go nie ma. Również to jest słuszne.
Dobra wiadomość: go fix przepisze za Ciebie stare literały. Więcej o tym później.
1.3. Wnioskowanie typów funkcji stało się mniej arbitralne
Wnioskowanie typów dla funkcji generycznych działało kiedyś w jednych miejscach, a w innych nie, bez żadnej widocznej zasady określającej, które miejsca są które. Przypisanie do zmiennej? W porządku. Umieszczenie dokładnie tej samej funkcji w polu struktury? Błąd kompilatora, proszę napisać double[int], jak w 2022 roku.
Go 1.27 sprawia, że wnioskowanie działa w każdym kontekście, w którym typ docelowy jest jednoznacznie znany:
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: wymagane double[int]
8 a := A{double} // 1.26: wymagane double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: wymagane double[int]
12
13 var fn func(float64) float64 = double // to zawsze działało
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Jedna funkcja double, zinstancjonowana jako func(int) int w trzech miejscach i jako func(float64) float64 w czwartym, w pełni na podstawie kontekstu. Jeśli piszesz struktury opcji lub tablice obsługi pełne wartości funkcyjnych, mnóstwo szumu [T] za chwilę zniknie z Twojej bazy kodu.
2. Środowisko uruchomieniowe: darmowa wydajność i detektor wycieków
2.1. Alokacja wyspecjalizowana rozmiarowo
Kompilator emituje teraz wywołania procedur alokacji wyspecjalizowanych rozmiarowo dla małych obiektów. Informacje o wydaniu podają do 30% mniej czasu na alokacje poniżej 80 bajtów i około 1% ogólnie dla programów intensywnie alokujących.
Nie wierzę w informacje o wydaniu bez pomiaru, więc:
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# domyślnie (size-specialized malloc włączony)
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% szybciej w mikrobenchmarku, który robi wyłącznie alokacje, na Apple Silicon. Innymi słowy: to najlepszy przypadek, osiągnięty w warunkach laboratoryjnych przez benchmark zaprojektowany tak, by liczba wyglądała dobrze. W prawdziwym programie GC i rzeczywista praca przysypią większość tego zysku. Twierdzenie o ok. 1% jest tym uczciwym.
Kosztem jest rozmiar pliku binarnego. Hello World:
12413202 bytes (domyślnie)
22361826 bytes (nosizespecializedmalloc)
Około 50 KB, stałe, niezależnie od programu. Można z tego zrezygnować przez GOEXPERIMENT=nosizespecializedmalloc, ale ta furtka ma zostać usunięta w Go 1.28, więc traktuj ją jako obejście na potrzeby zgłoszenia błędu, a nie jako styl życia.
2.2. Profil wycieków goroutine jest teraz prawdziwy
Eksperymentalny w Go 1.26, ogólnie dostępny w 1.27. Zmienna GOEXPERIMENT goroutineleakprofile zniknęła; po prostu działa.
1func leak() {
2 ch := make(chan int) // nikt nigdy nie wyśle. nikt.
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
Trzy goroutine, trwale zablokowane, z nazwą pliku i numerem linii wskazującymi dokładnie, gdzie je utworzyłeś.
Sztuczka stojąca za tym jest naprawdę sprytna: ponownie wykorzystuje analizę osiągalności z garbage collectora. Jeśli goroutine G jest zablokowana na prymitywie P, a P jest nieosiągalne z żadnej uruchomionej goroutine (ani z czegokolwiek, co te goroutines mogłyby obudzić), to nic nigdy więcej nie może dotknąć P, więc G nigdy się nie obudzi. To nie jest heurystyka, to dowód.
Ten sam projekt daje za darmo również ograniczenie: jeśli kanał lub mutex jest osiągalny przez zmienną globalną lub przez zmienną lokalną jakiejś wciąż działającej goroutine, GC nadal go widzi, więc runtime nie może niczego wywnioskować. Zwróć uwagę, że nawet powyższy prosty przykład wymaga dwóch wywołań runtime.GC(), aby cokolwiek zgłosić. Nie wyłapie wszystkiego.
Wyłapie jednak klasyczne "zapomniałem anulować kontekst, goroutine workera żyje wiecznie", czyli większość wycieków przez większość czasu.
Jeśli importujesz net/http/pprof, profil jest też dostępny pod /debug/pprof/goroutineleak. Podłącz to na stagingu, sprawdzaj raz w tygodniu i po cichu się przerażaj.
Tę pracę wniósł Vlad Saioc z Uber, któremu należy się napój dowolnego wyboru.
2.3. Traceback mówi teraz, które żądanie zginęło
Dla modułów deklarujących Go 1.27 lub nowszy, nagłówki tracebacku zawierają teraz etykiety goroutine z 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...
Ważny szczegół: etykiety są dziedziczone przez goroutines potomne. Goroutine 3 nigdy nie ustawiła etykiety. Dostała ją od rodzica.
Więc następnym razem, gdy na produkcji nastąpi deadlock i ktoś wyśle SIGQUIT do procesu, zamiast 4000 goroutines wyglądających identycznie, dostaniesz 4000 goroutines oznaczonych tym, do którego żądania i którego najemcy należą. Trzy linie pprof.Do w punkcie wejścia żądania zapewniają ci to.
Ponieważ etykiety mogą zawierać rzeczy, których nie chcesz zrzucać na stderr, GODEBUG=tracebacklabels=0 to wyłącza, a ta opcja rezygnacji ma być wyraźnie utrzymana na zawsze.
2.4. asynctimerchan odeszło na dobre
Go 1.23 uczyniło kanały timerów niebuforowanymi (synchronicznymi) i udostępniło asynctimerchan=1 jako sposób powrotu do dawnego zachowania. W 1.27 to ustawienie jest trwale usunięte. Kanały pakietu time są synchroniczne, bez dyskusji.
Interesująca jest polityka wprowadzona razem z tym. Usunięte GODEBUG pozostawione w Twoim go.mod nie psuje automatycznie builda; psuje go dopiero wtedy, gdy ustawiono je na stару wartość:
1# go.mod zawiera: 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 zawiera: godebug asynctimerchan=0
6(builds fine)
Oznacza to, że na reprymendę zasługują tylko ci, którzy rzeczywiście polegali na usuniętym zachowaniu. Każdy, kto ustawił wartość, która ostatecznie stała się domyślną, i o tym zapomniał, może nadal o tym nie myśleć. To przemyślany fragment archeologii API.
3. Biblioteka standardowa
3.1. encoding/json/v2: silnik wymieniono w locie
To jest duża zmiana. Ponad dwa lata dyskusji nad propozycją wreszcie dobiegły końca.
Są teraz trzy pakiety, a zrozumienie podziału to większość sukcesu:
| Pakiet | Zadanie |
|---|---|
encoding/json | Znane Ci API v1. Zachowanie w 100% niezmienione. Teraz zaimplementowane na bazie v2 |
encoding/json/v2 | Przetwarzanie semantyczne. Wartości Go ↔ JSON |
encoding/json/jsontext | Przetwarzanie składniowe. JSON jako strumień tokenów |
Najważniejsze jest to, że encoding/json zostało przebudowane na zupełnie innej implementacji i zachowuje się identycznie. Oto jak:
1// encoding/json.Unmarshal z Go 1.27, skrócone
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Każda starsza osobliwość v1 została zakodowana jako opcja, spakowana w DefaultOptionsV1(), a API v1 zawsze stosuje ten pakiet. Oznacza to, że poniższe zachowuje się tak samo w 1.26 i 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Tak, v1 nadal po cichu akceptuje zduplikowane klucze i bierze ostatni. Zawsze tak robiło. Nadal tak robi. Kompatybilność oznacza kompatybilność także ze złymi częściami.
v2 natomiast ma swoje zdanie:
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
Zduplikowane klucze i nieprawidłowy UTF-8 są odrzucane. Oba są naprawdę niebezpieczne, a nie tylko niechlujne: gdy dwa parsery nie zgadzają się co do tego, który zduplikowany klucz wygrywa, pojawiają się luki bezpieczeństwa. CVE-2017-12635 w CouchDB było dokładnie tym: treść JSON z dwoma kluczami roles, z których walidator odczytał jeden, a warstwa przechowywania drugi. Odrzucanie to właściwa decyzja.
Opcje są wariadyczne:
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// posortowane klucze mapy + wcięcia
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// odrzucanie nieznanych pól — wcześniej wymagało Decoder i 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 — większość rzeczy, które wcześniej rozwiązywano bibliotekami zewnętrznymi lub ręcznie pisanym MarshalJSON, jest teraz flagami.
A historia migracji jest najlepszą częścią. Późniejsze opcje mają pierwszeństwo, więc możesz przyjmować surowość v2 po jednym zachowaniu naraz:
1// zachowaj semantykę v1, ale odrzucaj zduplikowane klucze jak v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Nie musisz przenosić bazy kodu liczącej 200 tysięcy linii na v2, aby przestać akceptować zduplikowane klucze. Przełączasz jedną opcję. Przy dużych projektach to realistyczna droga.
Wydajność: marshalling jest mniej więcej na poziomie v1, unmarshalling jest znacznie szybszy. Gdyby coś poszło źle, GOEXPERIMENT=nojsonv2 przywraca starą implementację, a ta furtka również w końcu zostanie usunięta, więc zgłoś problem zamiast się w niej urządzać.
jsontext to warstwa niskopoziomowa: Encoder/Decoder przechodzące przez JSON jako Tokeny i Valuee z maszyną stanów, która pilnuje porządku. Sięgaj po nią, gdy piszesz strumieniowy transformator lub filtr JSON i nie chcesz wcale materializować wartości Go.
3.2. Standardowy pakiet uuid
Wreszcie. RFC 9562 w bibliotece standardowej, bez potrzeby go get.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: górne 48 bitów to znacznik czasu, więc te zawsze sortują się w kolejności utworzenia
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}
Całe API to New, NewV4, NewV7, Nil, Max, Parse, MustParse oraz jeden typ: type UUID [16]byte. To wszystko. Możesz przeczytać całą dokumentację pakietu, zanim ostygnie kawa.
Szczegóły warte uwagi:
UUIDto[16]byte, więc==działa i można go bezpośrednio użyć jako klucza mapy. Ten sam projekt cogoogle/uuid.NiliMaxsą funkcjami, a nie zmiennymi. Ponieważ pakietowevar Nil UUIDto nabity pistolet wycelowany w Twoją stopę, a ktoś, gdzieś, w końcu by do niej przypisał.- Losowe bity pochodzą z kryptograficznie bezpiecznego generatora.
- Implementuje
encoding.TextMarshaler/TextUnmarshaler/TextAppender, więc pasuje bezpośrednio do struktur JSON. NewV7to ten, którego chcesz dla kluczy głównych w bazie danych. Uporządkowanie czasowe oznacza, że indeks B-drzewa przestaje się fragmentować, w czym UUID v4 są notorycznie złe.
Możesz teraz usunąć zależność. Jeśli potrzebujesz v1/v3/v5 lub bardziej zaawansowanych opcji parsowania, biblioteki zewnętrzne nadal mają swoje zadanie.
3.3. crypto/mldsa: podpisy postkwantowe i są ogromne
Go 1.24 dało nam crypto/mlkem do postkwantowej wymiany kluczy. Go 1.27 wprowadza drugą połowę: podpisy ML-DSA, standaryzowane jako 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
Spójrz na te liczby. Pojedynczy podpis ma 3309 bajtów. Ed25519 ma 64. To wzrost około 50 razy, a klucz publiczny ma prawie 2 KB ponad to. Wrzuć kilka takich do łańcucha certyfikatów, a Twój handshake TLS zacznie potrzebować własnej strategii MTU.
To jest rzeczywisty koszt odporności kwantowej dzisiaj i dlatego nikt nie przełącza wszystkiego już jutro. Ale jest to teraz w bibliotece standardowej, czyli tam, gdzie chcesz, żeby było, zanim będziesz tego potrzebować.
Options.Context to separacja domen: podpisuj tym samym kluczem do różnych celów, używaj innego kontekstu dla każdego, a podpis z jednego kontekstu nie zweryfikuje się w innym. Powyższy przykład pokazuje dokładnie to: ten sam klucz, ta sama wiadomość, inny kontekst, odrzucone.
PrivateKey implementuje crypto.Signer, więc pasuje do istniejących interfejsów. crypto/x509 obsługuje klucze i podpisy ML-DSA, a crypto/tls obsługuje schematy podpisów MLDSA44/MLDSA65/MLDSA87 w TLS 1.3.
Jest też SignDeterministic, który pomija losowość, co przydaje się w testach i powtarzalnych buildach.
3.4. simd: instrukcje wektorowe bez assemblera
Go 1.26 wprowadziło eksperymentalnie specyficzny dla architektury simd/archsimd. Go 1.27 dodaje simd, który jest przenośny i niezależny od szerokości wektora. Włącza się przez 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 // obsługa ogona częściowym ładowaniem/zapisem — bez skalarnej pętli porządkowej
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]
Wybór projektowy, który ma znaczenie: szerokość wektora nigdy nie jest zakodowana na sztywno. Pytasz va.Len() w czasie wykonania. VectorBitSize() mówi, jaka jest rzeczywista szerokość, a Emulated() mówi, czy masz prawdziwy sprzęt, czy uprzejmą programową imitację. Mój Mac zgłosił 128-bitowy NEON; maszyna z AVX-512 zgłasza więcej; maszyna bez niczego zgłasza emulację, a kod nadal działa.
LoadFloat32sPart/StorePart zasługują na szczególną wzmiankę. Najbardziej uciążliwą częścią ręcznie pisanego SIMD jest zawsze nierówny ogon na końcu tablicy, a to obsługuje bez osobnej pętli skalarnej.
Nadal eksperymentalne, API nadal niestabilne, nie wdrażaj tego na produkcji. Ale fakt, że to kod Go, a nie assembler ani cgo, jest naprawdę wielką sprawą.
3.5. hash/maphash.Hasher
Nowy interfejs opisujący kontrakt między typem wartości a kontenerami opartymi na haszowaniu:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Dlaczego? Wbudowana mapa Go akceptuje tylko klucze comparable. Nie możesz użyć slice'a jako klucza. Nie możesz zdefiniować "równości z ignorowaniem wielkości liter". Hasher rozwiązuje oba problemy naraz:
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) // ten sam seed, bo inaczej cała ta zabawa nie ma sensu
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Dla zwykłej semantyki == istnieje ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Haczyk: Hasher to interfejs, a nie struktura danych. Nie ma jeszcze standardowej tablicy haszującej ani filtru Blooma, która by z niego korzystała. To fundament pod przyszły pakiet kontenerów. Dziś użyjesz go, budując własną strukturę, albo korzystając z istniejącej implementacji, jak go/types.Hasher (który pozwala użyć types.Type jako klucza mapy, respektując Identical).
Zarządzanie seedem spoczywa na Tobie. Gdyby hashOf powyżej tworzyło nowy seed przy każdym wywołaniu, identyczne wartości dawałyby różne skróty i przykład wypisałby false, czyli dokładnie błąd, który popełniłem przy pierwszej próbie. Jeden seed na kontener. (Seed jest losowy, aby uniemożliwić ataki DoS typu hash-flooding, dlatego nie jest po prostu stałą.)
3.6. httptest.NewTestServer + synctest.Sleep: trafienie w usypiacza
Jeśli z tego wydania przyjmiesz tylko jedną rzecz, niech to będzie ta.
testing/synctest weszło do użytku w Go 1.25 i dało nam sztuczny zegar do testowania współbieżnego kodu. Problem polegał na tym, że w chwili, gdy test dotknął prawdziwej sieci, iluzja się załamywała. httptest.NewTestServer w Go 1.27 używa wirtualnej sieci w pamięci, więc bańka pozostaje nienaruszona. A synctest.Sleep (= time.Sleep + synctest.Wait) dopełnia całość.
Oto test ponawiania z wykładniczym backoffem:
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 powinien wynosić dokładnie 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)
Przeczytaj to dwa razy. Zasymulowało trzy sekundy backoffu wobec prawdziwego serwera HTTP i zakończyło się w 0.00 sekundy. A asercja to elapsed == 3*time.Second, dokładnie trzy sekundy, a nie "co najmniej trzy sekundy, plus minus jitter harmonogramu". Sztuczne zegary nie mają jittera.
synctest.Sleep istnieje z konkretnego powodu: jeśli test śpi przez ten sam czas, co testowany kod, to który się obudzi pierwszy, jest czyjąś zgadywanką. synctest.Sleep śpi a potem czeka, aż każda inna goroutine w bańce będzie trwale zablokowana, więc obserwujesz system po tym, jak się ustabilizował.
Timeouty, ponowienia, wyłączniki obwodu, ograniczniki szybkości: każdy zależny od czasu test klienta HTTP, który posiadasz, może stać się szybki i deterministyczny. Jeśli Twój zestaw testów jest obecnie trzymany razem przez time.Sleep(100 * time.Millisecond) i nadzieję, to jest Twoje wyjście.
3.7. Zmiany w net/http, które naprawdę wpływają na produkcję
Ciche, ale pojawią się w Twoich metrykach.
Ciała odpowiedzi HTTP/1 są automatycznie opróżniane przy Close. Nieprzeczytana treść jest teraz opróżniana (do zachowawczego limitu) przy zamykaniu ciała, dzięki czemu połączenie może zostać ponownie użyte. Oznacza to, że w końcu możesz usunąć tę formułkę, którą wszyscy kopiują z tej samej odpowiedzi na Stack Overflow od 2016 roku:
1// nie jest już potrzebne
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Dla większości programów to brak efektu lub niewielki zysk. Jeśli sytuacja się pogorszy, prawdopodobnie znajdujesz się w kategorii, którą informacje o wydaniu grzecznie opisują: Transport.MaxIdleConns ustawione na 0 albo nowy Client na każde żądanie, co całkowicie omija limit bezczynnych połączeń. Transport.DisableKeepAlives = true zamaskuje to, ale rzeczywista rada z informacji o wydaniu brzmi, że "głębsza analiza prawdopodobnie byłaby korzystna", co w języku zespołu Go oznacza: masz większe problemy.
Priorytet klienta HTTP/2 (RFC 9218). Serwer honoruje teraz sygnały priorytetu klienta. Jeśli wolisz dawne szeregowanie round-robin, ustaw Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Ogranicza liczbę wartości nagłówków, jaką serwer zaakceptuje, domyślnie DefaultMaxHeaderValueCount. Kolejne drzwi zamknięte dla "wyślij dziesięć tysięcy nagłówków i zobacz, co się stanie".
ALPN na połączeniach dostarczonych przez użytkownika. Jeśli Twój net.Conn implementuje ConnectionState() tls.ConnectionState, Transport i Server wykonają na nim negocjację TLS ALPN, więc niestandardowe dialery przechodzące przez proxy nadal mogą negocjować HTTP/2.
3.8. Drobnostki, które wywołują radość
strings.CutLast / bytes.CutLast. Cut dzieli na pierwszym separatorze. Nie było wariantu "ostatni", więc wszyscy ręcznie pisali LastIndex i wycinanie, a około 30% z nas za pierwszym razem popełniło błąd o jeden.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Świetne do rozszerzeń plików i do parsowania host:port, gdzie trzeba znaleźć ostatni dwukropek, bo adresy IPv6 są ich pełne.
math/big.Int.Divide. Dzielenie z jawnym trybem zaokrąglania, zastępujące rzut monetą "czy chcę Quo czy 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
Jeśli pracujesz w dziedzinie, w której reguła zaokrąglania jest zapisana w przepisach, to jest dla Ciebie.
url.URL.Clone i url.Values.Clone. Kopie głębokie. Stara płytka kopia *u współdzieliła wskaźnik Userinfo, co dawało dokładnie taki błąd, którego znalezienie zajmuje dzień, a naprawa pięć sekund.
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
Metody odczytu net.UnixConn zwracają teraz io.EOF bezpośrednio, zamiast opakowywać go w net.OpError. err == io.EOF działa teraz, tak jak zawsze powinno.
database/sql.ConvertAssign i driver.RowsColumnScanner. Funkcje dla autorów sterowników. Pierwsza udostępnia konwersje typów, które wykonuje Rows.Scan; druga pozwala sterownikom skanować bezpośrednio do docelowych miejsc użytkownika, pomijając pośrednią alokację.
unicode 15 → 17. Dwie wersje jednym skokiem. Klasyfikacja napisów i zachowanie normalizacji mogą się subtelnie zmienić. Jeśli masz testy, które od tego zależą, dowiesz się o tym.
compress/flate działa szybciej — a jego bajty wyjściowe mogą różnić się od Go 1.26. To przenosi się na archive/zip, compress/gzip, compress/zlib i image/png. Jeśli masz testy golden-file, które haszują skompresowane dane wyjściowe, to się posypią, i nie będzie to regresja. Sprawdź to przed aktualizacją, a nie podczas przeglądu incydentu.
4. Toolchain
4.1. go fix zyskało więcej modernizatorów
go fix po cichu staje się narzędziem do modernizacji kodu. Go 1.27 dodaje atomictypes, embedlit, slicesbackward i unsafefuncs. Użyj -diff, aby zobaczyć podgląd:
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 }
Cztery modernizatory zadziałały na jednym małym pliku:
embedlit— przepisuje osadzone literały na nową składnię z §1.2atomictypes—atomic.AddInt64(&x, 1)staje sięatomic.Int64.Add(1), co uniemożliwia niatomowy dostęp i po cichu naprawia błędy wyrównania w 32-bitowych systemach, o których nie wiedziałeśslicesbackward— pętle wsteczne stają sięslices.Backwardwaitgroupgo— rytuałAdd/go/Donestaje sięwg.Go(zmieniono nazwę zwaitgroupz 1.26, aby uniknąć niejednoznaczności)
go tool fix help wymienia wszystkie 26; go tool fix help <name> wyjaśnia jeden z nich. Ponadto fmtappendf usunięto "ze względu na kwestie stylistyczne", co jest pięknie dyplomatycznym opisem tego, co wydarzyło się w wątku tego zgłoszenia.
Uruchomienie go fix ./... na starej bazie kodu to naprawdę satysfakcjonujące popołudnie. Oczywiście najpierw przeczytaj diff.
4.2. go test domyślnie uruchamia stdversion
To jest zmiana, która najprawdopodobniej przerwie Twój dzień.
go test uruchamia teraz domyślnie sprawdzenie vet stdversion, oznaczające symbole biblioteki standardowej nowsze niż dyrektywa go w Twoim go.mod:
1// go.mod mówi: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast to symbol z 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]
To eliminuje klasyczny tryb awarii, w którym wszystko działa na Twojej maszynie (najnowszy toolchain), a eksploduje w CI lub w starszym środowisku użytkownika. Jeśli publikujesz biblioteki z zachowawczą dyrektywą go, to prezent.
4.3. go test -json zyskało OutputType
Linie "Action":"output" zawierają teraz opcjonalne pole "OutputType": "error", "error-continue" lub "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'
Dane wyjściowe t.Log (bez pola), dane wyjściowe t.Error (error) i linie generowane przez framework (frame) są teraz rozróżnialne. Każdy, kto napisał wyrażenie regularne, by ustalić, które linie wyjścia testu są "prawdziwe", może je teraz usunąć.
4.4. Ulepszenia go doc
Składnia package@version. Czytaj dokumentację konkretnej wersji bez dodawania jej do swojego modułu:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Przydatne do sprawdzenia, co się zmieniło przed aktualizacją.
-ex i wypisywanie kodu przykładów:
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
Kod przykładu i oczekiwane wyjście, w terminalu, bez otwierania karty przeglądarki, która nadal będzie otwarta w przyszły czwartek.
4.5. go mod tidy porządkuje Twoje bloki require
Dla modułów z go 1.27 lub nowszym, go mod tidy scala rozproszone bloki require w co najwyżej dwa (bezpośrednie i pośrednie), zachowując dołączone komentarze.
1// Przed
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// Po: 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)
Zwróć uwagę, że komentarz // networking przetrwał. Ta zmiana głównie sprząta po rozwiązywaniu konfliktów scalania w Git, gdzie rodzą się błąkające się bloki require. Jeśli w Twoim zespole więcej niż trzy osoby dodają zależności, konflikty w Twoim go.mod staną się zauważalnie cichsze.
4.6. Różne drobiazgi
- Usunięto obsługę
bzr. Jeśli to Cię dotyczy, naprawdę chciałbym usłyszeć tę historię. go tool trace -httpwiąże się z localhost, gdy podano tylko port.-http=:6060już nie nasłuchuje na każdym interfejsie. Użyj-http=0.0.0.0:6060, jeśli tego właśnie chcesz. To ujednolica zachowanie zgo tool pprofi zapobiega sporadycznemu, przypadkowemu publicznemu udostępnieniu profilera.- Pliki odpowiedzi (
@file) są obsługiwane przezcompile,link,asm,cgo,coveripack, w formacie zgodnym z GCC. Dla systemów budowania, które przekraczają limity długości wiersza poleceń — witaj, Bazel.
5. Kompilator, linker, porty
Kompilator. Względne nazwy plików w dyrektywach //line są teraz rozwiązywane względem katalogu pliku zawierającego, zgodnie z go/scanner. Istotne, jeśli piszesz generatory kodu.
Nazwy symboli literałów funkcyjnych (domknięć) są też teraz prostsze — ta sama nazwa niezależnie od inlinowania, a wiele instancji tego samego literału może dzielić kod w pliku binarnym. Bez zmiany funkcjonalnej, z wyjątkiem: kod porównujący tożsamość funkcji przez reflect.Value.Pointer będzie częściej widział "równe" niż wcześniej. To porównanie nigdy nie było poprawne, ale jeśli je masz, teraz zacznie kłamać głośniej.
Linker. Nowe opcje -macos i -macsdk ustawiają wersje systemu i SDK w poleceniu ładującym LC_BUILD_VERSION na macOS.
Porty.
- Darwin wymaga teraz macOS 13 Ventura lub nowszego, jak zapowiedziano w 1.26. Sprawdź swoje runnery CI.
- PowerPC (
GOOS=linux GOARCH=ppc64) przeszło na ABI ELFv2. Wymaga jądra Linux 3.13+ (RHEL7 z backportem do 3.10). Cgo, PIE i linkowanie zewnętrzne są teraz obsługiwane. Jeśli używasz cgo, ale potrzebujesz statycznego, czysto Go pliku binarnego, ustawCGO_ENABLED=0.
6. Lista kontrolna aktualizacji
Go 1.27 poważnie traktuje kompatybilność, ale przed podniesieniem wersji sprawdź te punkty:
- Testy golden-file na skompresowanych danych wyjściowych.
compress/flatezmienił kodery; bajty gzip/zip/png mogą się różnić. - Testy dopasowujące komunikaty błędów JSON. Zachowanie jest identyczne; tekst błędu nie jest.
- Usunięte GODEBUG w
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— te psują build tylko wtedy, gdy ustawione są na swoje stare wartości. - Naruszenia
stdversion.go testwykrywa je teraz. Uruchom wcześnie, jeśli utrzymujesz bibliotekę z zachowawczą dyrektywągo. - Runnery CI z macOS 12 lub starszym. Wsparcie zniknęło.
- Testy sprawdzające nazwy symboli literałów funkcyjnych oraz porównania funkcji przez
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0alboClientna każde żądanie. W połączeniu z automatycznym opróżnianiem ciał odpowiedzi może to spowolnić działanie.
Końcowe przemyślenia
Go 1.27 to wydanie, w którym naraz upłynął termin wielu odłożonych prac konserwacyjnych.
Metody generyczne były brakującym elementem prac nad generykami z 1.18. Selektory pól w literałach struktur zamknęły jedenastoletnie zgłoszenie. encoding/json/v2 to dwa lata dyskusji nad propozycją. A uuid wypełnił lukę, którą praktycznie każdy projekt Go łatał tą samą zewnętrzną zależnością przez dekadę.
Jeśli chcesz kolejności priorytetów, by faktycznie wyciągnąć wartość z tego wydania:
httptest.NewTestServer+synctest.Sleep— testy HTTP zależne od czasu stają się szybkie i deterministyczne. Najlepszy stosunek wysiłku do korzyści w całym wydaniu.- Profil wycieków goroutine — udostępnij jeden endpoint na stagingu i przygotuj się na pokorę.
- Etykiety goroutine w tracebackach — trzy linie
pprof.Dow punkcie wejścia żądania zamieniają Twój następny zrzut stosu o 3 w nocy w coś czytelnego. go fix ./...— pozwól narzędziu zrobić modernizację.encoding/json/v2— bez pośpiechu. Przyjmuj to po jednej opcji naraz.
Weź go1.27rc3 i uruchom teraz swój zestaw testów. Wszystko z tej listy kontrolnej znacznie przyjemniej odkryć we wtorkowe popołudnie niż podczas incydentu.
Referencje