Go 1.27, czyli jak przestałem się martwić i pokochałem Generic Methods

Go 1.27 pojawi się w sierpniu 2026 roku i tym razem język sam w sobie uległ zmianie. Nie na zasadzie „dodaliśmy nową funkcję do slices” – on faktycznie się zmienił. Wprowadzono metody generyczne. Zamknięto jedenastoletnie zgłoszenie. encoding/json został po cichu zastąpiony nowym silnikiem w trakcie lotu.
Każdy z poniższych przykładów został uruchomiony na go1.27rc3 (darwin/arm64). Każdy blok wyjściowy jest rzeczywistym wynikiem, wklejonym metodą kopiuj-wklej, łącznie z komunikatami o błędach. Jeśli coś wygląda dziwnie, to dlatego, że właśnie tak zareagował kompilator.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Język się zmienił (tak, naprawdę)
1.1. Metody generyczne
Od wersji Go 1.18 mamy typy generyczne i od tego czasu toczy się „Ta Dyskusja”. Brzmi ona tak:
„Pozwólcie mi po prostu napisać metodę
Mapdla mojego typu slice—”
method must have no type parameters„...w porządku. Zostaje funkcja na poziomie pakietu.”
Metody mogły używać jedynie parametrów typów deklarowanych przez odbiorcę (receiver). Własna metoda nie mogła wprowadzać nowych. Dlatego każda transformacja generyczna była wygnana do zakresu pakietu, gdzie leżała obok siedemnastu innych swobodnych pomocników o nazwach będących wariantami MapSlice.
Go 1.27 to naprawia:
1type List[E any] []E
2
3// F jest parametrem typu zadeklarowanym 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}
Odbiorca nawet nie musi być generyczny. Zwykła struktura może posiadać metodę generyczną:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Zanim jednak dzisiejszego popołudnia przepiszesz całą swoją bazę kodu, musisz wiedzieć o dwóch ograniczeniach, które mają większe znaczenie, niż mogłoby się wydawać:
- Metody interfejsów nie mogą deklarować parametrów typów.
- Metody generyczne nie mogą implementować metod interfejsów.
To drugie ograniczenie jest tym, które sprawi ci kłopot:
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
Zatem metody generyczne i dynamiczne wywołania (dynamic dispatch) nie idą w parze. Nie wynika to ze skąpstwa zespołu Go — po prostu metoda generyczna ma nieskończenie wiele instancji, a tablica metod interfejsu musi być skończona i znana w czasie kompilacji. Nie można umieścić nieskończonego elementu w skończonej tablicy. Wszechświat powiedział „nie”.
Praktyczny wniosek: metody generyczne służą do konkretnych typów z API o charakterze narzędziowym. Wszystko, co musi ukrywać się za interfejsem, nadal wymaga starego podejścia.
Biblioteka standardowa już z tego skorzystała. Rand w math/rand/v2 otrzymał 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 tylko funkcja rand.N na poziomie pakietu była generyczna, co oznaczało używanie globalnego źródła. Teraz twój własny, zainicjowany *Rand otrzymuje to samo udogodnienie. Mała rzecz. Miła rzecz.
1.2. Selektory pól w literałach struktur, czyli: Issue #9859 wreszcie spoczywa w spokoju
Osadzanie (embedding) daje dostęp do u.ID. Osadzanie nie daje jednak możliwości zapisu User{ID: 1}. Zamiast tego otrzymujesz User{Base: Base{ID: 1}}, co jest sposobem Go na zapytanie, czy na pewno o to ci chodziło.
Issue #9859 zostało zgłoszone w 2015 roku. Teraz zostało zamknięte. Gdzieś tam gopher, który od tamtego czasu dwukrotnie zmienił zawód, otrzymuje powiadomienie na GitHubie.
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żyj bezpośrednio wypromowanego pola
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name nie jest polem Line. Jest polem osadzonego Object. Wcześniej musiałbyś napisać Line{Object: Object{name: "diagonal"}, ...}.
A teraz drobny druk, który sprawdziłem, próbując skompilować to wszystko.
(a) Kluczem nadal musi być zwykły identyfikator. Nie możesz napisać 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 funkcjonalność nie polega na tym, że „klucze działają teraz jak dostęp do pól”. Chodzi konkretnie o to, że „domyślnie wypromowane nazwy pól są dozwolone”. Nieco węższe znaczenie, niż sugerowałby nagłówek.
(b) Nie możesz jednocześnie określić osadzonego pola i czegoś, co zostało z niego wypromowane.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Rozsądne. Poinformowałeś kompilator o dwóch sprzecznych rzeczach dotyczących tego samego obszaru pamięci, więc odmówił zgadywania.
(c) Osadzanie wskaźników nie jest zaproszone.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Aby podążyć za wskaźnikiem, potrzebujesz wskaźnika, za którym można podążyć, a w czasie tworzenia literału jeszcze go nie ma. Również sprawiedliwe.
Dobra wiadomość: go fix przepisze stare literały za ciebie. 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 w niektórych miejscach, a w innych nie, bez wyraźnej zasady wyjaśniającej ten podział. Przypisanie do zmiennej? W porządku. Umieszczenie tej samej funkcji w polu struktury? Błąd kompilatora, proszę napisać double[int], jakby był rok 2022.
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: wymagało double[int]
8 a := A{double} // 1.26: wymagało double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: wymagało double[int]
12
13 var fn func(float64) float64 = double // to działało zawsze
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Jedno double, zainstancjonowane jako func(int) int w trzech miejscach i func(float64) float64 w czwartym, całkowicie na podstawie kontekstu. Jeśli piszesz struktury opcji lub tablice handlerów pełne wartości funkcyjnych, spora część szumu [T] wkrótce zniknie z twojej bazy kodu.
2. Runtime: darmowa wydajność i detektor wycieków
2.1. Alokacja wyspecjalizowana pod kątem rozmiaru
Kompilator emituje teraz wywołania procedur alokacji wyspecjalizowanych dla rozmiaru w przypadku małych obiektów. Informacje o wydaniu obiecują do 30% oszczędności przy alokacjach poniżej 80 bajtów i około 1% ogólnego zysku dla programów intensywnie alokujących pamięć.
Nie wierzę notkom o wydaniu bez pomiarów, 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# 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% szybciej w mikrobenchmarku, który nie robi nic poza alokacją na Apple Silicon. Co oznacza: to najlepszy przypadek, osiągnięty w warunkach laboratoryjnych przez benchmark zaprojektowany tak, by wyniki wyglądały dobrze. W rzeczywistym programie GC i realna praca przysłonią większość tego zysku. Obietnica ~1% jest tą uczciwą.
Koszt to rozmiar pliku binarnego. Hello World:
12413202 bajty (domyślnie)
22361826 bajtów (nosizespecializedmalloc)
Około 50 KB, na stałe, niezależnie od programu. Możesz zrezygnować z tej funkcji za pomocą GOEXPERIMENT=nosizespecializedmalloc, ale to wyjście awaryjne ma zostać usunięte w Go 1.28, więc traktuj je jako obejście problemu do zgłoszenia błędu, a nie styl życia.
2.2. Profil wycieków goroutines jest już rzeczywisty
Eksperymentalny w Go 1.26, ogólnie dostępny w 1.27. GOEXPERIMENT o nazwie goroutineleakprofile zniknął; 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 goroutines, trwale zablokowane, z plikiem i numerem linii wskazującymi dokładnie na to, gdzie popełniłeś błąd.
Sztuczka stojąca za tym jest prawdziwie genialna: wykorzystuje analizę osiągalności garbage collectora. Jeśli goroutine G jest zablokowana na prymitywie P, a P jest nieosiągalne z żadnej uruchomionej goroutine (lub czegokolwiek, co te goroutines mogłyby wybudzić), to nic nigdy nie dotknie P ponownie, więc G nigdy się nie obudzi. To nie jest heurystyka — to dowód.
Ta sama konstrukcja daje ograniczenie za darmo: jeśli kanał lub mutex jest osiągalny przez zmienną globalną lub przez zmienną lokalną jakiejś goroutine, która wciąż działa, GC nadal może go zobaczyć, więc runtime nie może wyciągnąć żadnych wniosków. Zauważ, że nawet powyższy przykład wymaga dwóch wywołań runtime.GC(), aby zgłosić problem. Nie wyłapie wszystkiego.
Wyłapie jednak klasyczne „zapomniałem anulować kontekst, goroutine pracownika żyje wiecznie” — co stanowi większość wycieków przez większość czasu.
Jeśli zaimportujesz net/http/pprof, znajdziesz to również pod adresem /debug/pprof/goroutineleak. Podepnij to w środowisku stagingowym, sprawdzaj co tydzień i bądź po cichu przerażony.
Ta praca została dostarczona przez Vlada Saioca z Ubera, któremu należy się napój według własnego wyboru.
2.3. Tracebacki teraz mówią, który request umarł
Dla modułów deklarujących Go 1.27 lub nowszy, linie nagłówka traceback 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 podrzędne goroutines. Goroutine 3 nigdy nie ustawiła etykiety. Otrzymała ją od rodzica.
Więc następnym razem, gdy produkcja zablokuje się (deadlock) i ktoś wyśle SIGQUIT do procesu, zamiast 4000 goroutines wyglądających identycznie, otrzymasz 4000 goroutines otagowanych informacją, do jakiego requestu i jakiego tenanta należą. Trzy linie pprof.Do w punkcie wejścia requestu załatwiają sprawę.
Ponieważ etykiety mogą zawierać rzeczy, których wolałbyś nie zrzucać do stderr, GODEBUG=tracebacklabels=0 wyłącza tę funkcję, a ta opcja wyłączenia ma pozostać na zawsze.
2.4. asynctimerchan zniknął na dobre
Go 1.23 uczyniło kanały timerów niebuforowanymi (synchronicznymi) i zaoferowało asynctimerchan=1 jako drogę powrotną do starego zachowania. W 1.27 to ustawienie jest trwale usunięte. Kanały pakietu time są synchroniczne, kropka, bez negocjacji.
Ciekawą częścią jest polityka wprowadzona wraz z tym. Usunięty GODEBUG pozostawiony w twoim go.mod nie przerywa automatycznie budowania — przerywa tylko wtedy, gdy jest ustawiony na starą 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(buduje się poprawnie)
Co oznacza, że jedynymi osobami, na które się krzyczy, są ci, którzy rzeczywiście polegali na usuniętym zachowaniu. Każdy, kto ustawił to na docelową wartość domyślną i o tym zapomniał, może przestać o tym myśleć. To przemyślany kawałek archeologii API.
3. Biblioteka standardowa
3.1. encoding/json/v2: Wymienili silnik w trakcie lotu
To jest najważniejsza zmiana. Ponad dwa lata dyskusji nad propozycją wreszcie przyniosły skutek.
Istnieją teraz trzy pakiety, a zrozumienie tego podziału to połowa sukcesu:
| Pakiet | Zadanie |
|---|---|
encoding/json | API v1, które znasz. Zachowanie w 100% niezmienione. Teraz zaimplementowane na v2 |
encoding/json/v2 | Przetwarzanie semantyczne. Wartości Go ↔ JSON |
encoding/json/jsontext | Przetwarzanie syntaktyczne. JSON jako strumień tokenów |
Nagłówkiem jest to, że encoding/json został zbudowany od nowa 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żdy dziwactwo v1 zostało zakodowane jako opcja, spakowane w DefaultOptionsV1(), a API v1 zawsze stosuje ten pakiet. Co oznacza, że to 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 cicho akceptuje duplikaty kluczy i bierze ostatni z nich. Zawsze to robiło. Nadal to robi. Kompatybilność oznacza kompatybilność również ze złymi częściami.
v2 natomiast ma swoje opinie:
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
Duplikaty kluczy i nieprawidłowe UTF-8 są odrzucane. Oba są autentycznie niebezpieczne, nie tylko niechlujne — kiedy dwa parsery nie zgadzają się co do tego, który duplikat klucza wygrywa, otrzymujesz błędy bezpieczeństwa. CVE-2017-12635 w CouchDB było dokładnie tym: ciało JSON z dwoma kluczami roles, gdzie walidator odczytał jeden, a warstwa przechowywania drugi. Odrzucenie 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// odrzuć nieznane pola — wcześniej wymagało to 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ązywałeś biblioteką zewnętrzną lub ręcznie napisanym MarshalJSON, to teraz flagi.
A historia migracji jest najlepszą częścią. Ostatnie opcje wygrywają, więc możesz przyjmować restrykcje v2 jedna po drugiej:
1// zachowaj semantykę v1, ale odrzucaj duplikaty kluczy tak jak v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Nie musisz portować 200-tysięcznej bazy kodu do v2, aby przestać akceptować duplikaty kluczy. Zmieniasz jedną opcję. Dla wszystkiego, co duże, jest to realistyczna ścieżka.
Wydajność: marshaling jest mniej więcej na tym samym poziomie, unmarshaling jest znacznie szybszy. Jeśli coś pójdzie nie tak, GOEXPERIMENT=nojsonv2 przywraca starą implementację — i to wyjście awaryjne również jest w końcu przeznaczone do usunięcia, więc zgłoś błąd zamiast się przyzwyczajać.
jsontext to niskopoziomowa warstwa: Encoder/Decoder przetwarzający JSON jako Tokeny i Valuey, z maszyną stanów pilnującą porządku. Sięgnij po to, gdy piszesz strumieniowy transformer lub filtr JSON i w ogóle nie chcesz materializować wartości Go.
3.2. Standardowy pakiet uuid
Wreszcie. RFC 9562 w bibliotece standardowej, żadne go get nie jest wymagane.
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 timestamp, więc 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 wystygnie twoja kawa.
Szczegóły warte uwagi:
UUIDto[16]byte, więc==działa i jest bezpośrednio używalne jako klucz mapy. Ten sam projekt co wgoogle/uuid.NiliMaxto funkcje, nie zmienne. Ponieważvar Nil UUIDna poziomie pakietu to naładowana broń wycelowana w twoją stopę, a ktoś, gdzieś, w końcu przypisałby do niej wartość.- Losowe bity pochodzą z kryptograficznie bezpiecznego generatora.
- Implementuje
encoding.TextMarshaler/TextUnmarshaler/TextAppender, więc wpada bezpośrednio do struktur JSON. NewV7jest tym, czego chcesz dla kluczy głównych w bazach danych. Kolejność czasowa oznacza, że twój indeks B-tree przestaje ulegać fragmentacji, w czym UUID v4 są słynne ze słabej wydajności.
Teraz możesz usunąć zależność. Jeśli potrzebujesz v1/v3/v5 lub bardziej wymyślnych opcji parsowania, biblioteki zewnętrzne nadal mają swoje miejsce.
3.3. crypto/mldsa: Podpisy post-kwantowe, i są one ogromne
Go 1.24 dało nam crypto/mlkem do wymiany kluczy post-kwantowych. Go 1.27 przynosi 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 50-krotny wzrost, a klucz publiczny ma dodatkowo prawie 2 KB. Upchnij kilka takich w łańcuchu certyfikatów, a twój handshake TLS zacznie wymagać własnej strategii MTU.
To jest rzeczywisty koszt odporności kwantowej dzisiaj i dlatego nikt nie przesiada się na nią z dnia na dzień. Ale jest to w bibliotece standardowej, co jest miejscem, w którym chcesz to mieć, zanim będziesz tego potrzebować.
Options.Context to separacja domen: podpisuj tym samym kluczem w różnych celach, użyj innego kontekstu dla każdego, a podpis z jednego kontekstu nie zostanie zweryfikowany w innym. Przykład powyżej 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 wspiera schematy podpisów MLDSA44/MLDSA65/MLDSA87 w TLS 1.3.
Istnieje również SignDeterministic, który pomija losowość — przydatne w testach i powtarzalnych buildach.
3.4. simd: Instrukcje wektorowe bez asemblera
Go 1.26 wprowadziło eksperymentalne, specyficzne dla architektury simd/archsimd. Go 1.27 dodaje simd — przenośne i niezależne od szerokości wektora. Włącz za pomocą 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łuż ogon częściowym load/store — bez pętli czyszczącej skalary
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 o va.Len() w czasie wykonywania. VectorBitSize() mówi ci o rzeczywistej szerokości, Emulated() mówi ci, czy masz prawdziwy sprzęt, czy uprzejmą symulację programową. Mój Mac zgłosił 128-bitowe NEON; maszyna AVX-512 zgłosi więcej; maszyna bez niczego zgłosi emulację i kod nadal działa.
LoadFloat32sPart/StorePart zasługują na szczególną uwagę. Najbardziej irytującą częścią ręcznie pisanego SIMD jest zawsze poszarpany ogon na końcu tablicy, a to rozwiązuje go bez osobnej pętli skalarnej.
Nadal eksperymentalne, API nadal niestabilne, nie umieszczaj tego na produkcji. Ale fakt, że to kod Go, a nie asembler czy cgo, to naprawdę wielka sprawa.
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ówny bez względu na wielkość 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) // to samo ziarno, inaczej nic z tego 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 czy filtra Blooma, który by go konsumował. To fundament pod przyszły pakiet kontenerów. Dzisiaj używasz tego przy budowaniu własnej struktury lub konsumowaniu istniejącej implementacji, takiej jak go/types.Hasher (która pozwala użyć types.Type jako klucza mapy, szanując Identical).
Zarządzanie ziarnem (seed) należy do ciebie. Jeśli hashOf powyżej tworzyłoby świeże ziarno przy każdym wywołaniu, identyczne wartości haszowałyby się inaczej i przykład drukowałby false — co jest dokładnie błędem, który napisałem przy pierwszej próbie. Jedno ziarno na kontener. (Ziarno jest randomizowane, aby pokonać ataki DoS typu hash-flooding, dlatego nie jest po prostu stałą.)
3.6. httptest.NewTestServer + synctest.Sleep: Hit, który uśpił konkurencję
Jeśli w tym wydaniu masz przyjąć tylko jedną rzecz, niech to będzie ta.
testing/synctest ukończyło studia w Go 1.25 i dało nam fałszywy zegar do testowania kodu współbieżnego. Z wyjątkiem momentu, gdy test dotknął prawdziwej sieci, iluzja pękała. httptest.NewTestServer w Go 1.27 używa wbudowanej fałszywej sieci w pamięci, więc bańka pozostaje nienaruszona. A synctest.Sleep (= time.Sleep + synctest.Wait) dopełnia całości.
Oto test 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 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. Symulowało trzy sekundy backoffu przeciwko prawdziwemu serwerowi HTTP i zakończyło się w 0,00 sekundy. A asercja to elapsed == 3*time.Second — dokładnie trzy sekundy, nie „przynajmniej trzy sekundy, z uwzględnieniem drgań schedulera”. Fałszywe zegary nie drżą.
synctest.Sleep istnieje z konkretnego powodu: jeśli twój test śpi przez ten sam czas, co testowany kod, to, kto obudzi się pierwszy, jest 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ł.
Timeouts, retries, circuit breakers, rate limiters — każdy test klienta HTTP zależny od czasu, który posiadasz, może stać się szybki i deterministyczny. Jeśli twój zestaw testów jest obecnie utrzymywany za pomocą time.Sleep(100 * time.Millisecond) i nadziei, to jest twoje wyjście.
3.7. Zmiany w net/http, które faktycznie wpływają na produkcję
Ciche, ale pojawią się w twoich metrykach.
Ciała odpowiedzi HTTP/1 automatycznie opróżniane przy Close. Nieprzeczytana zawartość jest teraz opróżniana (do konserwatywnego limitu), gdy zamykasz ciało, więc połączenie może zostać ponownie użyte. Co oznacza, że wreszcie możesz usunąć to zaklęcie, które każdy kopiował z tej samej odpowiedzi na Stack Overflow od 2016 roku:
1// już niepotrzebne
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Dla większości programów to operacja bez znaczenia lub mały zysk. Jeśli przez to sprawy idą gorzej, prawdopodobnie jesteś w grupie, którą notki o wydaniu uprzejmie opisują jako: Transport.MaxIdleConns ustawione na 0 lub nowy Client na każdy request, całkowicie omijający limit połączeń w stanie bezczynności. Transport.DisableKeepAlives = true załata to, ale rzeczywista rada w notkach brzmi: „głębsze spojrzenie byłoby korzystne”, co w języku zespołu Go oznacza masz większe problemy.
Priorytety klienta HTTP/2 (RFC 9218). Serwer teraz honoruje sygnały priorytetu klienta. Jeśli wolisz stare szeregowanie round-robin, użyj Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Ogranicza, ile wartości nagłówków serwer zaakceptuje, domyślnie DefaultMaxHeaderValueCount. Kolejne drzwi zamknięte na „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ą negocjację TLS ALPN na nim — więc customowe dialery przechodzące przez proxy mogą nadal negocjować HTTP/2.
3.8. Małe rzeczy, które budzą radość
strings.CutLast / bytes.CutLast. Cut dzieli przy pierwszym separatorze. Nie było wariantu „ostatni”, więc każdy ręcznie tworzył LastIndex plus slicing, a mniej więcej 30% z nas popełniło błąd o jeden (off-by-one) przy pierwszej próbie.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Świetne dla rozszerzeń plików i parsowania host:port — gdzie musisz znaleźć ostatni dwukropek, ponieważ adresy IPv6 są nimi wypełnione.
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 domenie, gdzie zasada zaokrąglania jest zapisana w regulacjach, to jest dla ciebie.
url.URL.Clone i url.Values.Clone. Głębokie kopie. Stara płytka kopia *u współdzieliła wskaźnik Userinfo, co produkował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
net.UnixConn metody odczytu zwracają teraz bezpośrednio io.EOF zamiast zawijać je w net.OpError. err == io.EOF teraz działa, tak jak powinno od zawsze.
database/sql.ConvertAssign i driver.RowsColumnScanner. Funkcje dla autorów sterowników. Pierwsza ujawnia konwersje typów wykonywane przez Rows.Scan; druga pozwala sterownikom skanować bezpośrednio do miejsc docelowych użytkownika, pomijając pośrednią alokację.
unicode 15 → 17. Dwie wersje w jednym skoku. Klasyfikacja ciągów i zachowanie normalizacji mogą się subtelnie zmienić. Jeśli masz testy, które od tego zależą, przekonasz się o tym.
compress/flate stał się szybszy — a jego bajty wyjściowe mogą różnić się od Go 1.26. To kaskadowo wpływa na archive/zip, compress/gzip, compress/zlib i image/png. Jeśli masz testy typu golden-file, które haszują skompresowane wyjście, one się zepsują i nie będzie to regresja. Sprawdź to przed aktualizacją, nie podczas przeglądu incydentu.
4. Toolchain
4.1. go fix zyskał więcej modernizatorów
go fix po cichu zmienia się w narzędzie do modernizacji kodu. Go 1.27 dodaje atomictypes, embedlit, slicesbackward i unsafefuncs. Użyj -diff do podglądu:
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 uruchomione na jednym małym pliku:
embedlit— przepisuje osadzone literały przy użyciu nowej składni z §1.2atomictypes—atomic.AddInt64(&x, 1)staje sięatomic.Int64.Add(1), co uniemożliwia nieatomowy dostęp i cicho naprawia błędy wyrównania 32-bitowego, o których nie wiedziałeśslicesbackward— pętle wsteczne stają sięslices.Backwardwaitgroupgo— rytuałAdd/go/Donestaje sięwg.Go(przemianowane zwaitgroupz 1.26, aby uniknąć niejednoznaczności)
go tool fix help wymienia wszystkie 26; go tool fix help <name> wyjaśnia jeden. Ponadto fmtappendf zostało usunięte „z powodów stylistycznych”, co jest pięknie dyplomatycznym sposobem na opisanie tego, co stało się w tym wątku zgłoszenia.
Uruchomienie go fix ./... na starej bazie kodu to autentycznie satysfakcjonujące popołudnie. Oczywiście najpierw przeczytaj diff.
4.2. go test uruchamia stdversion domyślnie
To zmiana, która najprawdopodobniej przerwie twój dzień.
go test uruchamia teraz domyślnie test stdversion, flagując symbole biblioteki standardowej nowsze niż pozwala na to 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 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 zabija klasyczny tryb awarii, w którym wszystko działa na twojej maszynie (najnowszy toolchain), a wybucha w CI lub w starszym środowisku użytkownika. Jeśli publikujesz biblioteki z konserwatywną dyrektywą go, to jest prezent.
4.3. go test -json zyskał OutputType
Linie "Action":"output" przenoszą 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'
Wyjście t.Log (brak pola), t.Error (error) i linie wygenerowane przez framework (frame) są teraz rozróżnialne. Każdy, kto napisał regexa, aby dowiedzieć się, które linie wyjścia testu są „prawdziwe”, może go teraz usunąć.
4.4. Ulepszenia go doc
Składnia package@version. Czytaj dokumentację dla konkretnej wersji bez dodawania jej do 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 sprawdzania, co się zmieniło przed aktualizacją.
-ex i drukowanie źródła 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
Źródło przykładu oraz oczekiwane wyjście, w terminalu, bez otwierania karty przeglądarki, która będzie otwarta jeszcze w następny czwartek.
4.5. go mod tidy czyści twoje bloki require
Dla modułów na go 1.27 lub nowszym, go mod tidy łączy rozproszone bloki require w co najwyżej dwa (direct i indirect), 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)
Zauważ, że komentarz // networking przetrwał. To głównie czyści po rozwiązywaniu konfliktów scalania Git, gdzie rodzą się zabłąkane bloki require. Jeśli twój zespół ma więcej niż trzy osoby dodające zależności, twoje konflikty w go.mod będą teraz zauważalnie cichsze.
4.6. Różności
- Wsparcie dla
bzrusunięte. 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=:6060nie nasłuchuje już na każdym interfejsie. Użyj-http=0.0.0.0:6060, jeśli to miałeś na myśli. Teraz pasuje dogo tool pprofi zapobiega okazjonalnemu przypadkowemu publicznemu profilerowi.- Pliki odpowiedzi (
@file) są obsługiwane przezcompile,link,asm,cgo,coveripackw formacie kompatybilnym z GCC. Dla systemów budowania, które przekraczają limity długości linii poleceń — witaj, Bazel.
5. Kompilator, Linker, Porty
Kompilator. Względne nazwy plików w dyrektywach //line są teraz rozwiązywane względem katalogu zawierającego plik, pasując do go/scanner. Istotne, jeśli piszesz generatory kodu.
Nazwy symboli literałów funkcji (domknięć) są teraz prostsze — ta sama nazwa niezależnie od inliningu, a wiele instancji tego samego literału może współdzielić kod w pliku binarnym. Brak zmiany funkcjonalnej, z wyjątkiem: kod porównujący tożsamość funkcji przez reflect.Value.Pointer będzie widział „równe” częściej niż wcześniej. To porównanie nigdy nie było poprawne, ale jeśli je masz, teraz zacznie ci kłamać głośniej.
Linker. Nowe opcje -macos i -macsdk ustawiają wersje OS i SDK w komendzie ładowania LC_BUILD_VERSION w systemie macOS.
Porty.
- Darwin wymaga teraz macOS 13 Ventura lub nowszego, zgodnie z zapowiedzią w 1.26. Sprawdź swoje CI runnerzy.
- PowerPC (
GOOS=linux GOARCH=ppc64) przełączył się na ABI ELFv2. Wymaga jądra Linux 3.13+ (RHEL7 backportowane do 3.10). Cgo, PIE i linkowanie zewnętrzne są teraz obsługiwane. Jeśli używasz cgo, ale potrzebujesz statycznego, czystego binarki Go, ustawCGO_ENABLED=0.
6. Lista kontrolna aktualizacji
Go 1.27 traktuje kompatybilność poważnie, ale sprawdź to przed podniesieniem wersji:
- Testy golden-file na skompresowanym wyjściu.
compress/flatezmieniło enkodery; bajty gzip/zip/png mogą się różnić. - Testy dopasowujące ciągi komunikatów 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— one przerywają budowanie tylko wtedy, gdy są ustawione na swoje stare wartości. - Naruszenia
stdversion.go testwyłapuje je teraz. Uruchom wcześnie, jeśli utrzymujesz bibliotekę z konserwatywną dyrektywągo. - CI runnerzy z macOS 12 lub starszym. Wsparcie zniknęło.
- Testy sprawdzające nazwy symboli literałów funkcji oraz porównania funkcji
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0lubClientna request. W połączeniu z automatycznym opróżnianiem ciał odpowiedzi może to zwolnić.
Przemyślenia końcowe
Go 1.27 to wydanie, w którym wiele odroczonych prac technicznych stało się wymagalnych naraz.
Metody generyczne były brakującym elementem prac nad typami generycznymi z 1.18. Selektory pól w literałach struktur zamknęły jedenastoletnie zgłoszenie. encoding/json/v2 to dwa lata dyskusji nad propozycjami. A uuid wypełnił dziurę, którą w zasadzie każdy projekt w Go łatał tą samą zewnętrzną zależnością przez dekadę.
Jeśli chcesz priorytetową kolejność czerpania korzyści z tego wydania:
httptest.NewTestServer+synctest.Sleep— testy HTTP zależne od czasu stają się szybkie i deterministyczne. Najlepszy stosunek wysiłku do zysku w całym wydaniu.- Profil wycieków goroutine — wystaw jeden endpoint w stagingu i przygotuj się na pokorę.
- Etykiety goroutine w tracebackach — trzy linie
pprof.Dow punkcie wejścia requestu zamieniają twój następny zrzut stosu o 3 nad ranem w coś czytelnego. go fix ./...— pozwól narzędziu wykonać modernizację.encoding/json/v2— bez pośpiechu. Przyjmuj to jedna opcja na raz.
Pobierz go1.27rc3 i uruchom na nim swój zestaw testów teraz. Wszystko z tej listy kontrolnej jest znacznie przyjemniej odkryć we wtorkowe popołudnie niż podczas incydentu.
Referencje