Go 1.27, ou comment j'ai appris à ne plus m'en faire et à aimer les Generic Methods

Go 1.27 arrive en août 2026, et pour une fois, le langage lui-même a changé. Pas un changement du type « nous avons ajouté une nouvelle fonction à slices » — un vrai changement. Les méthodes génériques sont là. Un problème vieux de onze ans a été résolu. encoding/json a été discrètement remplacé par un nouveau moteur alors que l'avion était en plein vol.
Chaque exemple ci-dessous a été exécuté sur go1.27rc3 (darwin/arm64). Chaque bloc de sortie est une sortie réelle, copiée-collée, incluant les messages d'erreur. Si quelque chose semble étrange, c'est parce que c'est ce que le compilateur a réellement affiché.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Le langage a changé (Oui, vraiment)
1.1. Méthodes génériques
Depuis Go 1.18, nous disposons des génériques, et depuis Go 1.18, nous avons « La Conversation ». Elle se déroule comme suit :
« Laisse-moi juste écrire une méthode
Mapsur mon type slice— »
method must have no type parameters« ...très bien. Ce sera une fonction au niveau du package. »
Les méthodes ne pouvaient utiliser que des paramètres de type déclarés par le récepteur. Votre propre méthode ne pouvait pas en introduire de nouveaux. Ainsi, chaque transformation générique était exilée à la portée du package, où elle côtoyait dix-sept autres fonctions utilitaires flottantes nommées selon une variante de MapSlice.
Go 1.27 corrige cela :
1type List[E any] []E
2
3// F est un paramètre de type déclaré par la méthode elle-même
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}
Le récepteur n'a même pas besoin d'être générique. Une structure classique peut avoir une méthode générique :
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Maintenant, avant de refactoriser toute votre base de code cet après-midi, il existe deux restrictions, et elles ont plus d'importance qu'il n'y paraît :
- Les méthodes d'interface ne peuvent pas déclarer de paramètres de type.
- Les méthodes génériques ne peuvent pas implémenter de méthodes d'interface.
La seconde est celle qui vous posera problème :
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
Ainsi, les méthodes génériques et la répartition dynamique (dynamic dispatch) ne font pas bon ménage. Ce n'est pas l'équipe Go qui se montre avare — c'est qu'une méthode générique possède une infinité d'instanciations, et qu'une table de méthodes d'interface doit être finie et connue au moment de la compilation. Vous ne pouvez pas mettre une chose infinie dans une table finie. L'univers a dit non.
En pratique : les méthodes génériques sont destinées aux types concrets avec des API de type utilitaire. Tout ce qui doit se cacher derrière une interface nécessite toujours l'ancienne approche.
La bibliothèque standard en a déjà profité. Rand de math/rand/v2 a reçu une méthode générique :
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
Auparavant, seule la fonction rand.N au niveau du package était générique, ce qui signifiait utiliser la source globale. Désormais, votre propre *Rand initialisé bénéficie de la même commodité. Une petite chose. Une chose agréable.
1.2. Sélecteurs de champs dans les littéraux de structure, ou : Le problème #9859 trouve enfin le repos
L'imbrication (embedding) vous donne accès à u.ID. L'imbrication ne vous donne pas accès à User{ID: 1}. Au lieu de cela, elle vous donne User{Base: Base{ID: 1}}, ce qui est la façon dont Go vous demande si c'est vraiment ce que vous vouliez.
Le problème #9859 a été ouvert en 2015. Il est maintenant fermé. Quelque part, un gopher ayant changé deux fois de carrière reçoit une notification 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 : utilisez directement le champ promu
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name n'est pas un champ de Line. C'est un champ de l'objet Object imbriqué. Auparavant, vous auriez écrit Line{Object: Object{name: "diagonal"}, ...}.
Passons maintenant aux détails, que j'ai vérifiés en essayant réellement de tout compiler.
(a) La clé reste un identifiant simple. Vous ne pouvez pas écrire un chemin de sélecteur arbitraire.
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
La fonctionnalité ne signifie donc pas « les clés fonctionnent désormais comme l'accès aux champs ». Il s'agit spécifiquement de « les noms de champs implicitement promus sont autorisés ». Un peu plus restreint que ce que suggère le titre.
(b) Vous ne pouvez pas spécifier un champ imbriqué et un élément promu à partir de celui-ci en même temps.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Logique. Vous lui avez donné deux instructions contradictoires concernant la même mémoire et il a refusé de deviner.
(c) L'imbrication de pointeurs n'est pas invitée.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Pour suivre un pointeur, il faut un pointeur à suivre, et au moment de la construction du littéral, il n'y en a pas encore. C'est également juste.
Bonne nouvelle : go fix réécrira vos anciens littéraux pour vous. Plus d'informations à ce sujet plus tard.
1.3. L'inférence de type de fonction est devenue moins arbitraire
L'inférence de type pour les fonctions génériques fonctionnait par endroits et pas dans d'autres, sans principe discernable expliquant pourquoi. Affectation à une variable ? Très bien. Mettre la même fonction dans un champ de structure ? Erreur de compilation, veuillez écrire double[int] comme en 2022.
Go 1.27 rend l'inférence fonctionnelle dans chaque contexte où le type cible est sans ambiguïté connu :
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 : nécessitait double[int]
8 a := A{double} // 1.26 : nécessitait double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26 : nécessitait double[int]
12
13 var fn func(float64) float64 = double // cela a toujours fonctionné
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Un seul double, instancié en tant que func(int) int à trois endroits et func(float64) float64 à un quatrième, entièrement à partir du contexte. Si vous écrivez des structures d'options ou des tables de gestionnaires remplies de valeurs de fonction, une grande partie du bruit [T] est sur le point de disparaître de votre base de code.
2. Runtime : Performance gratuite et détecteur de fuites
2.1. Allocation spécialisée par taille
Le compilateur émet désormais des appels vers des routines d'allocation spécialisées par taille pour les petits objets. Les notes de version annoncent jusqu'à 30 % d'économie sur les allocations de moins de 80 octets, et environ 1 % au total pour les programmes gourmands en allocations.
Je ne crois pas les notes de version sans mesurer, alors :
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 % plus rapide dans un micro-benchmark qui ne fait qu'allouer sur Apple Silicon. Ce qui revient à dire : c'est le meilleur cas, obtenu dans des conditions de laboratoire par un benchmark conçu pour rendre le chiffre attrayant. Dans un programme réel, le GC et le travail réel masqueront la majeure partie de cet avantage. L'affirmation de ~1 % est l'honnête.
Le coût est la taille du binaire. Hello World :
12413202 bytes (default)
22361826 bytes (nosizespecializedmalloc)
Environ 50 Ko, fixe, quel que soit votre programme. Vous pouvez désactiver cela avec GOEXPERIMENT=nosizespecializedmalloc, mais cette porte de sortie est prévue pour être supprimée dans Go 1.28, donc traitez-la comme une solution temporaire de rapport de bug plutôt que comme une habitude.
2.2. Le profil de fuite de Goroutine est maintenant réel
Expérimental dans Go 1.26, généralement disponible dans 1.27. Le GOEXPERIMENT goroutineleakprofile a disparu ; cela fonctionne tout simplement.
1func leak() {
2 ch := make(chan int) // personne n'enverra jamais. personne.
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
Trois goroutines, bloquées en permanence, avec un fichier et un numéro de ligne pointant exactement là où vous avez fait l'erreur.
L'astuce derrière cela est véritablement intelligente : elle réutilise l'analyse d'accessibilité du garbage collector. Si une goroutine G est bloquée sur une primitive P, et que P est inaccessible depuis toute goroutine exécutable (ou tout ce que ces goroutines pourraient réveiller), alors rien ne pourra plus jamais toucher P, donc G ne se réveillera jamais. Ce n'est pas une heuristique — c'est une preuve.
La même conception vous donne la limitation gratuitement : si le canal ou le mutex est accessible via une variable globale, ou via une variable locale d'une goroutine toujours en cours d'exécution, le GC peut toujours le voir, donc le runtime ne peut rien conclure. Notez que même l'exemple jouet ci-dessus nécessite deux appels à runtime.GC() pour signaler le problème. Il ne détectera pas tout.
Cependant, il détectera le classique « oubli d'annuler le contexte, la goroutine de travail vit éternellement » — ce qui représente la plupart des fuites la plupart du temps.
Si vous importez net/http/pprof, il est également disponible à /debug/pprof/goroutineleak. Intégrez cela dans votre environnement de staging, vérifiez-le chaque semaine, et soyez discrètement horrifié.
Ce travail a été contribué par Vlad Saioc chez Uber, qui mérite une boisson de son choix.
2.3. Les traces (tracebacks) vous indiquent maintenant quelle requête a échoué
Pour les modules déclarant Go 1.27 ou ultérieur, les lignes d'en-tête des traces incluent désormais les étiquettes de goroutine 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...
Le détail important : les étiquettes sont héritées par les goroutines enfants. La goroutine 3 n'a jamais défini d'étiquette. Elle en a reçu une de son parent.
Ainsi, la prochaine fois que la production sera bloquée et que quelqu'un enverra un SIGQUIT au processus, au lieu de 4 000 goroutines qui se ressemblent toutes, vous obtiendrez 4 000 goroutines étiquetées avec la requête et le client auxquels elles appartiennent. Trois lignes de pprof.Do à votre point d'entrée de requête vous offrent cela.
Comme les étiquettes peuvent contenir des éléments que vous préférez ne pas envoyer vers stderr, GODEBUG=tracebacklabels=0 les désactive, et cette option de désactivation est explicitement prévue pour rester indéfiniment.
2.4. asynctimerchan a disparu pour de bon
Go 1.23 a rendu les canaux de timer non bufferisés (synchrones) et a proposé asynctimerchan=1 comme moyen de revenir à l'ancien comportement. Dans 1.27, ce paramètre est définitivement supprimé. Les canaux du package time sont synchrones, point final, aucune négociation.
La partie intéressante est la politique introduite parallèlement. Un GODEBUG supprimé laissé dans votre go.mod ne casse pas automatiquement la build — il ne casse que s'il est défini sur l'ancienne valeur :
1# go.mod contains: 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 contains: godebug asynctimerchan=0
6(builds fine)
Ce qui signifie que les seules personnes qui se font réprimander sont celles qui comptaient réellement sur le comportement supprimé. Tous ceux qui l'ont défini sur la valeur par défaut finale et l'ont oublié continuent de ne pas avoir à y penser. C'est une pièce réfléchie d'archéologie d'API.
3. Bibliothèque standard
3.1. encoding/json/v2 : Ils ont remplacé le moteur en plein vol
C'est le gros morceau. Plus de deux ans de discussion sur la proposition ont finalement abouti.
Il existe désormais trois packages, et comprendre cette séparation représente l'essentiel du défi :
| Package | Rôle |
|---|---|
encoding/json | L'API v1 que vous connaissez. Comportement 100 % inchangé. Désormais implémenté au-dessus de v2 |
encoding/json/v2 | Traitement sémantique. Valeurs Go ↔ JSON |
encoding/json/jsontext | Traitement syntaxique. JSON en tant que flux de jetons (token stream) |
Le titre est que encoding/json a été reconstruit sur une implémentation complètement différente et se comporte de manière identique. Voici comment :
1// Go 1.27's encoding/json.Unmarshal, abridged
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Chaque particularité de la v1 héritée a été encodée en tant qu'option, regroupée dans DefaultOptionsV1(), et l'API v1 applique toujours ce groupe. Ce qui signifie que cela se comporte de la même manière dans 1.26 et 1.27 :
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Oui, la v1 accepte toujours silencieusement les clés en double et prend la dernière. Elle l'a toujours fait. Elle le fait toujours. La compatibilité signifie aussi la compatibilité avec les mauvais aspects.
La v2, quant à elle, a des opinions :
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
Les clés en double et l'UTF-8 invalide sont rejetés. Les deux sont réellement dangereux, pas seulement malpropres — lorsque deux analyseurs ne sont pas d'accord sur la clé en double qui gagne, vous obtenez des failles de sécurité. La CVE-2017-12635 de CouchDB était exactement cela : un corps JSON avec deux clés roles, où le validateur en lisait une et la couche de stockage l'autre. Le rejet est la bonne décision.
Les options sont variadiques :
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// clés de carte triées + indentation
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// rejeter les champs inconnus — nécessitait auparavant un Decoder et 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 — la plupart des choses que vous résolviez auparavant avec une bibliothèque tierce ou un MarshalJSON écrit à la main sont désormais des options.
Et l'histoire de la migration est la meilleure partie. Les dernières options l'emportent, vous pouvez donc adopter la rigueur de la v2 comportement par comportement :
1// garder la sémantique v1, mais rejeter les clés en double comme le fait la v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Vous n'avez pas besoin de porter une base de code de 200 000 lignes vers la v2 pour arrêter d'accepter les clés en double. Vous basculez une option. Pour tout projet d'envergure, c'est la voie réaliste.
Performances : le marshaling est à peu près à parité, l'unmarshaling est significativement plus rapide. Si cela tourne mal, GOEXPERIMENT=nojsonv2 restaure l'ancienne implémentation — et cette option est également sur la sellette à terme, alors ouvrez un ticket plutôt que de vous installer dans cette solution.
jsontext est la couche de bas niveau : Encoder/Decoder parcourant le JSON en tant que Tokens et Values avec une machine à états vous gardant honnête. Utilisez-la lorsque vous écrivez un transformateur de flux ou un filtre JSON et que vous ne voulez pas matérialiser des valeurs Go du tout.
3.2. Un package uuid standard
Enfin. RFC 9562, dans la bibliothèque standard, aucun go get requis.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7 : les 48 bits supérieurs sont un horodatage, donc ceux-ci sont toujours triés par ordre de création
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}
Toute l'API est composée de New, NewV4, NewV7, Nil, Max, Parse, MustParse, et d'un type : type UUID [16]byte. C'est tout. Vous pouvez lire toute la documentation du package pendant que votre café refroidit.
Détails à connaître :
UUIDest[16]byte, donc==fonctionne et il est directement utilisable comme clé de carte. Même conception quegoogle/uuid.NiletMaxsont des fonctions, pas des variables. Parce qu'unevar Nil UUIDau niveau du package est un fusil chargé pointé sur votre pied, et quelqu'un, quelque part, finirait par lui assigner une valeur.- Les bits aléatoires proviennent d'un générateur cryptographiquement sécurisé.
- Il implémente
encoding.TextMarshaler/TextUnmarshaler/TextAppender, donc il s'intègre directement dans les structures JSON. NewV7est celui que vous voulez pour les clés primaires de base de données. L'ordre temporel signifie que votre index B-tree cesse de se fragmenter, ce pour quoi les UUID v4 sont notoirement mauvais.
Vous pouvez maintenant supprimer une dépendance. Si vous avez besoin de v1/v3/v5 ou d'options d'analyse plus sophistiquées, les bibliothèques tierces ont toujours un rôle à jouer.
3.3. crypto/mldsa : Signatures post-quantiques, et elles sont énormes
Go 1.24 nous a donné crypto/mlkem pour l'échange de clés post-quantique. Go 1.27 apporte l'autre moitié : les signatures ML-DSA, standardisées sous le nom de 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
Regardez ces chiffres. Une seule signature fait 3 309 octets. Ed25519 en fait 64. C'est une multiplication par 50, et la clé publique fait près de 2 Ko en plus. Insérez quelques-uns de ces éléments dans une chaîne de certificats et votre handshake TLS commence à nécessiter sa propre stratégie MTU.
C'est le coût réel de la résistance quantique aujourd'hui, et c'est pourquoi personne ne bascule tout demain. Mais c'est maintenant dans la bibliothèque standard, ce qui est là où vous voulez que ce soit avant d'en avoir besoin.
Options.Context est une séparation de domaine : signez avec la même clé à des fins différentes, utilisez un contexte différent pour chacune, et une signature d'un contexte ne sera pas vérifiée dans un autre. L'exemple ci-dessus montre exactement cela — même clé, même message, contexte différent, rejeté.
PrivateKey implémente crypto.Signer, donc il s'insère dans les interfaces existantes. crypto/x509 gère les clés et signatures ML-DSA, et crypto/tls prend en charge les schémas de signature MLDSA44/MLDSA65/MLDSA87 en TLS 1.3.
Il existe également SignDeterministic, qui ignore le caractère aléatoire — pratique pour les tests et les builds reproductibles.
3.4. simd : Instructions vectorielles sans assembleur
Go 1.26 a introduit le simd/archsimd spécifique à l'architecture en tant qu'expérience. Go 1.27 ajoute simd — portable et agnostique quant à la largeur des vecteurs. Activez avec 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 // gérer la fin avec un chargement/stockage partiel — pas de boucle de nettoyage scalaire
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]
Le choix de conception qui compte : la largeur du vecteur n'est jamais codée en dur. Vous demandez va.Len() au moment de l'exécution. VectorBitSize() vous indique la largeur réelle, Emulated() vous indique si vous avez du matériel réel ou une imitation logicielle polie. Mon Mac a rapporté du NEON 128 bits ; une machine AVX-512 en rapporte plus ; une machine sans rien rapporte une émulation et le code s'exécute quand même.
LoadFloat32sPart/StorePart méritent une mention spéciale. La partie la plus ennuyeuse de l'écriture manuelle SIMD est toujours la fin irrégulière du tableau, et ceci le gère sans boucle scalaire séparée.
Toujours expérimental, API toujours instable, ne mettez pas cela en production. Mais le fait que ce soit du code Go et non de l'assembleur ou du cgo est une chose réellement importante.
3.5. hash/maphash.Hasher
Une nouvelle interface décrivant le contrat entre un type de valeur et les conteneurs basés sur le hachage :
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Pourquoi ? La carte (map) intégrée de Go n'accepte que les clés comparable. Vous ne pouvez pas utiliser un slice comme clé. Vous ne pouvez pas définir « égal en ignorant la casse ». Hasher résout les deux à la fois :
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) // même graine (seed), sinon rien de tout cela n'a de sens
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Pour les sémantiques == ordinaires, il existe ComparableHasher[T] :
1hashOf(maphash.ComparableHasher[int]{}, 42)
Le piège : Hasher est une interface, pas une structure de données. Il n'existe pas encore de table de hachage ou de filtre de Bloom standard qui la consomme. C'est le travail préparatoire pour un futur package de conteneurs. Aujourd'hui, vous l'utiliseriez lors de la construction de votre propre structure, ou en consommant une implémentation existante comme go/types.Hasher (qui vous permet d'utiliser types.Type comme clé de carte, en respectant Identical).
La gestion de la graine (seed) vous incombe. Si hashOf ci-dessus créait une nouvelle graine à chaque appel, des valeurs identiques auraient des hachages différents et l'exemple imprimerait false — ce qui est exactement le bug que j'ai écrit lors de ma première tentative. Une graine par conteneur. (La graine est randomisée pour contrer les attaques DoS par inondation de hachage, c'est pourquoi ce n'est pas juste une constante.)
3.6. httptest.NewTestServer + synctest.Sleep : Le succès surprise
Si vous ne deviez adopter qu'une seule chose de cette version, faites que ce soit celle-ci.
testing/synctest a obtenu son diplôme dans Go 1.25 et nous a donné une fausse horloge pour tester le code concurrent. Sauf qu'au moment où votre test touchait un vrai réseau, l'illusion s'effondrait. Le httptest.NewTestServer de Go 1.27 utilise un faux réseau en mémoire, donc la bulle reste intacte. Et synctest.Sleep (= time.Sleep + synctest.Wait) complète le tout.
Voici un test de nouvelle tentative avec backoff exponentiel :
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 // le backoff devrait être exactement 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)
Lisez cela deux fois. Il a simulé trois secondes de backoff contre un vrai serveur HTTP, et a terminé en 0,00 seconde. Et l'assertion est elapsed == 3*time.Second — exactement trois secondes, pas « au moins trois secondes, à plus ou moins une gigue du planificateur ». Les fausses horloges n'ont pas de gigue.
synctest.Sleep existe pour une raison spécifique : si votre test dort pendant la même durée que le code testé, personne ne peut deviner lequel se réveille en premier. synctest.Sleep dort puis attend que chaque autre goroutine dans la bulle soit durablement bloquée, vous observez donc le système après qu'il se soit stabilisé.
Délais d'attente, tentatives, disjoncteurs, limiteurs de débit — chaque test de client HTTP dépendant du temps que vous possédez peut devenir rapide et déterministe. Si votre suite de tests est actuellement maintenue ensemble avec time.Sleep(100 * time.Millisecond) et de l'espoir, voici votre porte de sortie.
3.7. Changements dans net/http qui affectent réellement la production
Silencieux, mais ils apparaîtront dans vos métriques.
Les corps de réponse HTTP/1 se drainent automatiquement lors du Close. Le contenu non lu est maintenant drainé (jusqu'à une limite conservatrice) lorsque vous fermez le corps, afin que la connexion puisse être réutilisée. Ce qui signifie que vous pouvez enfin supprimer cette incantation que tout le monde a copiée-collée de la même réponse Stack Overflow depuis 2016 :
1// n'est plus nécessaire
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Pour la plupart des programmes, c'est une opération nulle ou une petite victoire. Si cela empire les choses, vous êtes probablement dans la catégorie que les notes de version décrivent poliment : Transport.MaxIdleConns défini sur 0, ou un nouveau Client par requête, contournant totalement la limite de connexion inactive. Transport.DisableKeepAlives = true masquera le problème, mais le conseil réel des notes de version est qu'« un examen plus approfondi serait probablement bénéfique », ce qui, dans le langage de l'équipe Go, signifie vous avez des problèmes plus graves.
Priorité client HTTP/2 (RFC 9218). Le serveur respecte désormais les signaux de priorité du client. Si vous préférez l'ancien ordonnancement round-robin, Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Limite le nombre de valeurs d'en-tête que le serveur acceptera, par défaut DefaultMaxHeaderValueCount. Une porte de plus fermée sur « envoyer dix mille en-têtes et regarder ce qui se passe ».
ALPN sur les connexions fournies par l'utilisateur. Si votre net.Conn implémente ConnectionState() tls.ConnectionState, Transport et Server effectueront une négociation TLS ALPN dessus — ainsi, les dialers personnalisés passant par un proxy peuvent toujours négocier HTTP/2.
3.8. Les petites choses qui suscitent la joie
strings.CutLast / bytes.CutLast. Cut divise au premier séparateur. Il n'y avait pas de variante « dernier », donc tout le monde écrivait manuellement LastIndex plus le découpage, et environ 30 % d'entre nous faisaient une erreur d'un caractère lors de la première tentative.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Idéal pour les extensions de fichiers et pour l'analyse de hôte:port — où vous devez trouver le dernier deux-points, car les adresses IPv6 en sont remplies.
math/big.Int.Divide. Division avec un mode d'arrondi explicite, remplaçant le pile ou face « est-ce Quo ou Div que je veux ? » :
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
Si vous travaillez dans un domaine où la règle d'arrondi est écrite dans un règlement, ceci est pour vous.
url.URL.Clone et url.Values.Clone. Copies profondes (deep copies). L'ancienne copie superficielle *u partageait le pointeur Userinfo, ce qui produisait exactement le genre de bug qui prend une journée à trouver et cinq secondes à corriger.
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 : les méthodes de lecture retournent désormais io.EOF directement au lieu de l'envelopper dans net.OpError. err == io.EOF fonctionne maintenant, comme il aurait toujours dû le faire.
database/sql.ConvertAssign et driver.RowsColumnScanner. Fonctionnalités pour les auteurs de drivers. La première expose les conversions de type que Rows.Scan effectue ; la seconde permet aux drivers de scanner directement dans les destinations de l'utilisateur, en sautant une allocation intermédiaire.
unicode 15 → 17. Deux versions en un saut. Le comportement de classification et de normalisation des chaînes peut changer subtilement. Si vous avez des tests qui en dépendent, vous le découvrirez.
compress/flate est devenu plus rapide — et ses octets de sortie peuvent différer de Go 1.26. Cela se répercute sur archive/zip, compress/gzip, compress/zlib et image/png. Si vous avez des tests basés sur des fichiers de référence (golden-files) qui hachent la sortie compressée, ils vont casser, et ce ne sera pas une régression. Vérifiez cela avant de mettre à jour, pas pendant l'analyse d'incident.
4. Chaîne d'outils (Toolchain)
4.1. go fix a ajouté plus de moderniseurs
go fix se transforme discrètement en un outil de modernisation de code. Go 1.27 ajoute atomictypes, embedlit, slicesbackward et unsafefuncs. Utilisez -diff pour prévisualiser :
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 }
Quatre moderniseurs ont été déclenchés sur un petit fichier :
embedlit— réécrit les littéraux imbriqués en utilisant la nouvelle syntaxe de la §1.2atomictypes—atomic.AddInt64(&x, 1)devientatomic.Int64.Add(1), ce qui rend l'accès non atomique impossible et corrige discrètement des bugs d'alignement 32 bits dont vous ignoriez l'existenceslicesbackward— les boucles inversées deviennentslices.Backwardwaitgroupgo— le rituelAdd/go/Donedevientwg.Go(renommé depuis lewaitgroupde 1.26 pour éviter toute ambiguïté)
go tool fix help liste les 26 ; go tool fix help <name> en explique un. De plus, fmtappendf a été supprimé « pour des raisons stylistiques », ce qui est une façon magnifiquement diplomatique de décrire ce qui s'est passé dans ce fil de discussion.
Exécuter go fix ./... sur une ancienne base de code est un après-midi vraiment satisfaisant. Lisez le diff d'abord, évidemment.
4.2. go test exécute stdversion par défaut
C'est le changement le plus susceptible d'interrompre votre journée.
go test exécute désormais la vérification stdversion par défaut, signalant les symboles de la bibliothèque standard plus récents que ce que permet la directive go de votre go.mod :
1// go.mod dit : go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast est un symbole 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]
Cela tue le mode d'échec classique où tout fonctionne sur votre machine (dernière chaîne d'outils) et explose en CI ou dans l'environnement plus ancien d'un utilisateur. Si vous publiez des bibliothèques avec une directive go conservatrice, c'est un cadeau.
4.3. go test -json a gagné OutputType
Les lignes "Action":"output" portent désormais un champ optionnel "OutputType" : "error", "error-continue" ou "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'
La sortie t.Log (pas de champ), la sortie t.Error (error) et les lignes générées par le framework (frame) sont désormais distinguables. Quiconque a écrit une expression régulière pour déterminer quelles lignes de sortie de test sont « réelles » peut maintenant la supprimer.
4.4. Améliorations de go doc
Syntaxe package@version. Lisez la documentation pour une version spécifique sans l'ajouter à votre module :
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Pratique pour vérifier ce qui a changé avant une mise à jour.
-ex et impression de la source des exemples :
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
Source de l'exemple et sortie attendue, dans le terminal, sans ouvrir un onglet de navigateur qui sera encore ouvert jeudi prochain.
4.5. go mod tidy nettoie vos blocs require
Pour les modules en go 1.27 ou ultérieur, go mod tidy fusionne les blocs require dispersés en au plus deux (direct et indirect), en préservant les commentaires attachés.
1// Avant
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// Après : 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)
Notez que le commentaire // networking a survécu. Cela nettoie principalement après la résolution des conflits de fusion Git, qui est l'endroit où naissent les blocs require errants. Si votre équipe compte plus de trois personnes ajoutant des dépendances, vos conflits go.mod sont sur le point de devenir nettement plus calmes.
4.6. Divers
- Support
bzrsupprimé. Si cela vous affecte, j'aimerais vraiment entendre l'histoire. go tool trace -httpse lie à localhost lorsqu'on ne lui donne qu'un port.-http=:6060n'écoute plus sur toutes les interfaces. Utilisez-http=0.0.0.0:6060si c'était votre intention. Cela correspond désormais àgo tool pprofet empêche le profileur public accidentel occasionnel.- Les fichiers de réponse (
@file) sont pris en charge parcompile,link,asm,cgo,coveretpack, dans un format compatible GCC. Pour les systèmes de build qui dépassent les limites de longueur de ligne de commande — bonjour, Bazel.
5. Compilateur, Éditeur de liens, Ports
Compilateur. Les noms de fichiers relatifs dans les directives //line se résolvent désormais par rapport au répertoire du fichier contenant, correspondant à go/scanner. Pertinent si vous écrivez des générateurs de code.
Les noms de symboles des littéraux de fonction (fermetures) sont également plus simples maintenant — le même nom indépendamment de l'inlining, et plusieurs instances du même littéral peuvent partager du code dans le binaire. Aucun changement fonctionnel, sauf : le code qui compare l'identité des fonctions via reflect.Value.Pointer verra « égal » plus souvent qu'avant. Cette comparaison n'a jamais été valide, mais si vous l'avez, c'est maintenant qu'elle commence à vous mentir plus fort.
Éditeur de liens (Linker). Nouvelles options -macos et -macsdk définissant les versions d'OS et de SDK dans la commande de chargement LC_BUILD_VERSION de macOS.
Ports.
- Darwin nécessite désormais macOS 13 Ventura ou plus récent, comme annoncé dans 1.26. Vérifiez vos runners CI.
- PowerPC (
GOOS=linux GOARCH=ppc64) est passé à l'ABI ELFv2. Nécessite le noyau Linux 3.13+ (RHEL7 rétroporté vers 3.10). Cgo, PIE et l'édition de liens externe sont désormais pris en charge. Si vous utilisez cgo mais avez besoin d'un binaire Go pur statique, définissezCGO_ENABLED=0.
6. Liste de contrôle de mise à jour
Go 1.27 prend la compatibilité au sérieux, mais vérifiez ces points avant d'augmenter la version :
- Tests de fichiers de référence (golden-file) sur la sortie compressée.
compress/flatea changé ses encodeurs ; les octets gzip/zip/png peuvent différer. - Tests correspondant aux chaînes de messages d'erreur JSON. Le comportement est identique ; le texte de l'erreur ne l'est pas.
- GODEBUG supprimés dans
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf— ceux-ci font échouer la build uniquement s'ils sont définis sur leurs anciennes valeurs. - Violations de
stdversion.go testles détecte maintenant. Exécutez-le tôt si vous maintenez une bibliothèque avec une directivegoconservatrice. - Runners CI macOS 12 ou plus ancien. Le support est terminé.
- Tests s'appuyant sur les noms de symboles des littéraux de fonction, et comparaisons de fonctions
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0ou unClientpar requête. Combiné avec le drainage automatique des corps de réponse, cela peut devenir plus lent.
Réflexions finales
Go 1.27 est la version où beaucoup de maintenance différée est arrivée à échéance en même temps.
Les méthodes génériques étaient la pièce manquante du travail sur les génériques de 1.18. Les sélecteurs de champs dans les littéraux de structure ont clôturé un problème vieux de onze ans. encoding/json/v2 représentait deux ans de discussion sur les propositions. Et uuid a comblé un vide que pratiquement tous les projets Go patchaient avec la même dépendance tierce depuis une décennie.
Si vous voulez un ordre de priorité pour réellement tirer profit de cette version :
httptest.NewTestServer+synctest.Sleep— les tests HTTP dépendants du temps deviennent rapides et déterministes. Le meilleur ratio effort/gain de toute la version.- Le profil de fuite de goroutine — exposez un endpoint en staging et préparez-vous à être humble.
- Étiquettes de goroutine de traceback — trois lignes de
pprof.Doà votre point d'entrée de requête transforment votre prochain dump de pile de 3h du matin en quelque chose de lisible. go fix ./...— laissez l'outil effectuer la modernisation.encoding/json/v2— pas de précipitation. Adoptez-le une option à la fois.
Récupérez go1.27rc3 et exécutez votre suite de tests dessus maintenant. Tout ce qui figure sur cette liste de contrôle est nettement plus agréable à découvrir un mardi après-midi que pendant un incident.
Références