Go 1.27, ou comment j'ai appris à cesser de m'inquiéter et à aimer les méthodes génériques


Go 1.27 arrive en août 2026, et cette fois le langage lui-même a changé. Pas au sens « nous avons ajouté une nouvelle fonction à slices » : il a réellement changé. Les méthodes génériques sont désormais disponibles. Un problème vieux de onze ans a été fermé. 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é avec go1.27rc3 (darwin/arm64). Chaque bloc de sortie est une sortie réelle, copiée telle quelle, y compris les messages d'erreur. Si quelque chose semble étrange, c'est parce que c'est exactement ce que le compilateur a 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 connaissons aussi The Conversation. Elle se déroule ainsi :
« Laissez-moi simplement é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 les paramètres de type déclarés par le receveur. Votre propre méthode ne pouvait pas en introduire de nouveaux. Ainsi, chaque transformation générique était exilée au niveau du package, où elle côtoyait dix-sept autres fonctions auxiliaires flottantes, nommées selon diverses variantes 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 receveur n'a même pas besoin d'être générique. Une simple structure peut posséder 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 l'intégralité de votre base de code cet après-midi, sachez qu'il existe deux restrictions, et qu'elles comptent davantage 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 des méthodes d'interface.
C'est cette seconde restriction qui vous causera des ennuis :
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 le dispatch dynamique ne font pas bon ménage. Ce n'est pas que l'équipe Go soit avare : une méthode générique possède une infinité d'instanciations, alors qu'une table de méthodes d'interface doit être finie et connue à la compilation. On ne peut pas placer une chose infinie dans une table finie. L'univers a dit non.
Lecture pratique : les méthodes génériques sont destinées aux types concrets dotés d'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é. Le 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 de package rand.N était générique, ce qui impliquait d'utiliser la source globale. Désormais, votre propre *Rand initialisé avec une graine bénéficie de la même commodité. Petite chose. Belle chose.
1.2. Sélecteurs de champs dans les littéraux de structure, ou : le problème n° 9859 trouve enfin son repos
L'embarquement vous donne u.ID. L'embarquement ne vous donne pas User{ID: 1}. Il vous donne plutôt User{Base: Base{ID: 1}}, ce qui est la façon qu'a Go de vous demander si vous le vouliez vraiment.
Le problème n° 9859 a été ouvert en 2015. Il vient d'être fermé. Quelque part, un gopher qui a depuis 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 : utiliser 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'Object embarqué. Auparavant, il fallait écrire Line{Object: Object{name: "diagonal"}, ...}.
Voici maintenant les détails, que j'ai vérifiés en essayant réellement de compiler l'ensemble.
(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é n'est donc pas « les clés fonctionnent désormais comme des accès aux champs ». Elle permet précisément les noms de champs promus implicitement. C'est un peu plus restreint que ce que laisse entendre le titre.
(b) Vous ne pouvez pas spécifier simultanément un champ embarqué et un élément promu à partir de celui-ci.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Raisonnable. Vous avez donné deux informations contradictoires sur la même zone mémoire, et il a refusé de deviner.
(c) L'embarquement de pointeurs n'est pas encouragé.
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'en existe pas encore. Là encore, c'est juste.
Bonne nouvelle : go fix réécrira vos anciens littéraux à votre place. Nous y reviendrons plus tard.
1.3. L'inférence de types de fonctions est devenue moins arbitraire
L'inférence de types pour les fonctions génériques fonctionnait autrefois à certains endroits et pas à d'autres, sans principe discernable. Affectation à une variable ? Pas de problème. Placer exactement la même fonction dans un champ de structure ? Erreur du compilateur, veuillez écrire double[int] comme en 2022.
Go 1.27 fait fonctionner l'inférence dans chaque contexte où le type cible est connu sans ambiguïté :
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 func(int) int à trois endroits et en 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 fonctions, une multitude de [T] superflus va bientôt disparaître de votre base de code.
2. Runtime : performances gratuites 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 % de gain sur les allocations de moins de 80 octets, et environ 1 % globalement pour les programmes à forte allocation.
Je ne crois pas aux notes de version sans mesure, donc :
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# par défaut (malloc spécialisé par taille activé)
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 microbenchmark qui ne fait rien d'autre qu'allouer sur Apple Silicon. Autrement dit : c'est le meilleur cas, obtenu en conditions de laboratoire par un benchmark conçu pour faire paraître le chiffre flatteur. Dans un programme réel, le GC et le travail effectif masqueront l'essentiel de ce gain. L'affirmation d'environ 1 % est la plus honnête.
Le coût se situe au niveau de la taille du binaire. Hello World :
12413202 bytes (par défaut)
22361826 bytes (nosizespecializedmalloc)
Environ 50 Ko, de façon fixe, quel que soit votre programme. Vous pouvez désactiver cette fonctionnalité avec GOEXPERIMENT=nosizespecializedmalloc, mais cette échappatoire est programmée pour être supprimée dans Go 1.28. Considérez-la donc comme une solution de contournement pour un rapport de bogue, et non comme un mode de vie.
2.2. Le profil de fuite de goroutines est désormais réel
Expérimental dans Go 1.26, disponible en général dans la 1.27. La GOEXPERIMENT goroutineleakprofile a disparu : cela fonctionne simplement.
1func leak() {
2 ch := make(chan int) // personne ne enverra jamais rien. 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, définitivement bloquées, avec un nom de fichier et un numéro de ligne pointant exactement vers l'endroit où vous les avez créées.
Le principe derrière cela est vraiment astucieux : il réutilise l'analyse d'accessibilité du ramasse-miettes. Si la goroutine G est bloquée sur la primitive P, et si P est inaccessible depuis toute goroutine exécutable (ou depuis tout ce que ces goroutines pourraient réveiller), alors plus rien ne peut toucher P, et G ne se réveillera donc jamais. Ce n'est pas une heuristique : c'est une preuve.
La même conception vous offre 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 encore le voir, et le runtime ne peut donc rien conclure. Remarquez que même l'exemple jouet ci-dessus nécessite deux appels à runtime.GC() pour produire un rapport. Il n'attrapera pas tout.
Il détectera toutefois le classique « j'ai oublié d'annuler le contexte, la goroutine de travail vit éternellement », qui constitue la plupart des fuites la plupart du temps.
Si vous importez net/http/pprof, il est également accessible à /debug/pprof/goroutineleak. Intégrez-le à votre environnement de préproduction, consultez-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 d'appels indiquent désormais quelle requête a échoué
Pour les modules déclarant Go 1.27 ou une version ultérieure, les lignes d'en-tête des traces incluent désormais les étiquettes de goroutines de 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 se bloquera 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 locataire auxquels elles appartiennent. Trois lignes de pprof.Do à votre point d'entrée de requêtes vous offrent cela.
Comme les étiquettes peuvent contenir des éléments que vous ne souhaitez pas voir affichés sur stderr, GODEBUG=tracebacklabels=0 désactive la fonctionnalité, et cette option de désactivation est explicitement destinée à persister indéfiniment.
2.4. asynctimerchan a définitivement disparu
Go 1.23 a rendu les canaux de minuterie non bufferisés (synchrones) et proposé asynctimerchan=1 comme moyen de revenir à l'ancien comportement. Dans la 1.27, ce paramètre est définitivement supprimé. Les canaux du package time sont synchrones, point final, sans 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 compilation : il ne la casse que s'il est défini à l'ancienne valeur :
1# go.mod contient : 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 contient : godebug asynctimerchan=0
6(compile sans problème)
Cela signifie que les seules personnes réprimandées sont celles qui dépendaient réellement du comportement supprimé. Tous ceux qui ont défini le paramètre à la valeur par défaut future et l'ont oublié peuvent continuer à ne pas y penser. C'est une belle pièce d'archéologie d'API.
3. Bibliothèque standard
3.1. encoding/json/v2 : le moteur a été remplacé en plein vol
C'est le gros morceau. Plus de deux ans de discussions sur la proposition ont enfin abouti.
Il existe désormais trois paquets, et comprendre cette répartition constitue l'essentiel de la bataille :
| Paquet | Rôle |
|---|---|
encoding/json | L'API v1 que vous connaissez. Comportement inchangé à 100 %. Désormais implémentée au-dessus de la v2 |
encoding/json/v2 | Traitement sémantique. Valeurs Go ↔ JSON |
encoding/json/jsontext | Traitement syntaxique. JSON comme flux de jetons |
Le point principal est que encoding/json a été reconstruit sur une implémentation entièrement différente et se comporte de manière identique. Voici comment :
1// encoding/json.Unmarshal de Go 1.27, résumé
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Chaque particularité héritée de la v1 a été encodée sous forme d'option, regroupée dans DefaultOptionsV1(), et l'API v1 applique toujours ce groupe. Ce qui signifie que le code suivant se comporte de la même manière en 1.26 et en 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 dupliquées et retient la dernière. Elle l'a toujours fait. Elle le fait encore. La compatibilité signifie la compatibilité avec les mauvaises parties également.
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 dupliquées et l'UTF-8 invalide sont rejetés. Les deux sont réellement dangereux, et pas seulement désordonnés : lorsque deux analyseurs ne s'accordent pas sur la clé dupliquée qui doit l'emporter, on obtient des failles de sécurité. La CVE-2017-12635 de CouchDB en est exactement l'illustration : un corps JSON contenant deux clés roles, où le validateur en lisait une et la couche de stockage l'autre. Rejeter est le bon choix.
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 map 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 problèmes que vous résolviez auparavant avec une bibliothèque tierce ou un MarshalJSON écrit à la main sont désormais des options.
Et la stratégie de migration est la meilleure partie. Les options ultérieures l'emportent, de sorte que vous pouvez adopter la rigueur de la v2 un comportement à la fois :
1// conserver la sémantique v1, mais rejeter les clés dupliquées 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 cesser d'accepter les clés dupliquées. Vous modifiez une seule option. Pour tout ce qui est volumineux, c'est la voie réaliste.
Performances : la sérialisation est à peu près à parité, la désérialisation est nettement plus rapide. En cas de problème, GOEXPERIMENT=nojsonv2 restaure l'ancienne implémentation. Cette échappatoire est elle aussi destinée à disparaître à terme ; ouvrez donc un ticket plutôt que de vous y installer.
jsontext est la couche bas niveau : Encoder/Decoder parcourent le JSON sous forme de Token et de Value, avec une machine à états qui vous maintient honnête. Utilisez-le lorsque vous écrivez un transformateur en flux ou un filtre JSON et que vous ne voulez pas du tout matérialiser des valeurs Go.
3.2. Un paquet uuid standard
Enfin. RFC 9562, dans la bibliothèque standard, sans go get.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7 : les 48 bits de poids fort sont un horodatage, ils se trient donc toujours dans l'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}
L'ensemble de l'API se compose de New, NewV4, NewV7, Nil, Max, Parse, MustParse, et d'un seul type : type UUID [16]byte. C'est tout. Vous pouvez lire la documentation complète du paquet pendant que votre café refroidit.
Détails utiles à connaître :
UUIDest un[16]byte, donc==fonctionne et il peut être utilisé directement comme clé de map. Même conception quegoogle/uuid.NiletMaxsont des fonctions, et non des variables. Car unevar Nil UUIDau niveau du paquet est un pistolet chargé pointé sur votre pied, et quelqu'un, quelque part, finirait par lui affecter une valeur.- Les bits aléatoires proviennent d'un générateur cryptographiquement sûr.
- Il implémente
encoding.TextMarshaler/TextUnmarshaler/TextAppender, il s'intègre donc directement dans les structures JSON. NewV7est celui qu'il vous faut pour les clés primaires de bases de données. L'ordre temporel fait que votre index B-tree cesse de se fragmenter, ce que les UUID v4 font notoirement mal.
Vous pouvez désormais supprimer une dépendance. Si vous avez besoin des v1/v3/v5 ou d'options d'analyse plus sophistiquées, les bibliothèques tierces ont encore leur utilité.
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, normalisé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 augmentation d'un facteur 50, et la clé publique fait près de 2 Ko en plus. Insérez quelques-unes de ces signatures dans une chaîne de certificats, et votre handshake TLS devra commencer à gérer sa propre stratégie de MTU.
C'est le coût réel de la résistance quantique aujourd'hui, et c'est pourquoi personne ne migre tout dès demain. Mais cela figure désormais dans la bibliothèque standard, ce qui est exactement là où vous voulez qu'elle soit avant d'en avoir besoin.
Options.Context assure la séparation des domaines : signez avec la même clé à des fins différentes, utilisez un contexte différent pour chacune, et une signature issue d'un contexte ne sera pas vérifiée dans un autre. L'exemple ci-dessus le montre exactement : même clé, même message, contexte différent, rejet.
PrivateKey implémente crypto.Signer, il s'insère donc 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 dans TLS 1.3.
Il existe aussi SignDeterministic, qui ignore l'aléa, pratique pour les tests et les builds reproductibles.
3.4. simd : instructions vectorielles sans assembleur
Go 1.26 a introduit simd/archsimd, spécifique à l'architecture, à titre expérimental. Go 1.27 ajoute simd, portable et indépendant de la largeur vectorielle. À activer 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 // traiter le reste avec un chargement/stockage partiel : pas de boucle scalaire de nettoyage
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 interrogez va.Len() à l'exécution. VectorBitSize() indique la largeur réelle, Emulated() indique si vous disposez d'un véritable matériel ou d'une imitation logicielle polie. Mon Mac a signalé du NEON 128 bits ; une machine AVX-512 en signale davantage ; une machine sans rien signale une émulation, et le code s'exécute quand même.
LoadFloat32sPart/StorePart méritent une mention particulière. La partie la plus pénible du SIMD écrit à la main est toujours la fin irrégulière du tableau, et celle-ci la gère sans boucle scalaire distincte.
Toujours expérimental, API encore instable, à ne pas mettre en production. Mais le fait que cela soit du code Go, et non de l'assembleur ou du cgo, constitue une avancée réellement importante.
3.5. hash/maphash.Hasher
Une nouvelle interface décrivant le contrat entre un type 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 map native de Go n'accepte que des clés comparable. Vous ne pouvez pas utiliser une slice comme clé. Vous ne pouvez pas définir « égal en ignorant la casse ». Hasher résout les deux problèmes à 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, sinon tout cela n'a aucun sens
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Pour la sémantique ordinaire de ==, il existe ComparableHasher[T] :
1hashOf(maphash.ComparableHasher[int]{}, 42)
Le piège : Hasher est une interface, pas une structure de données. Aucune table de hachage standard ni aucun filtre de Bloom ne l'utilise encore. C'est une base pour un futur paquet de conteneurs. Aujourd'hui, vous l'utiliseriez en construisant votre propre structure, ou en consommant une implémentation existante comme go/types.Hasher (qui vous permet d'utiliser types.Type comme clé de map, en respectant Identical).
La gestion de la graine vous incombe. Si hashOf ci-dessus générait une nouvelle graine à chaque appel, des valeurs identiques produiraient des hachages différents, et l'exemple afficherait false : c'est exactement le bogue que j'ai écrit lors de ma première tentative. Une graine par conteneur. (La graine est aléatoire pour contrer les attaques DoS par saturation de hachage, c'est pourquoi ce n'est pas simplement une constante.)
3.6. httptest.NewTestServer + synctest.Sleep : le coup du dormeur
Si vous n'adoptez qu'une seule chose de cette version, prenez celle-ci.
testing/synctest est passé au stade stable dans Go 1.25 et nous a donné une horloge factice pour tester le code concurrent. Mais dès que votre test touchait un réseau réel, l'illusion s'effondrait. httptest.NewTestServer de Go 1.27 utilise un réseau factice en mémoire, de sorte que la bulle reste intacte. Et synctest.Sleep (= time.Sleep + synctest.Wait) complète le tableau.
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 doit ê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)
Relisez cela deux fois. Il a simulé trois secondes de backoff face à un véritable serveur HTTP, et a terminé en 0,00 seconde. Et l'assertion est elapsed == 3*time.Second, soit exactement trois secondes, et non « au moins trois secondes, à la gigue de l'ordonnanceur près ». Les horloges factices ne gigotent pas.
synctest.Sleep existe pour une raison précise : si votre test dort pendant la même durée que le code testé, on ne sait pas lequel se réveille en premier. synctest.Sleep dort puis attend que toutes les autres goroutines de la bulle soient durablement bloquées, de sorte que vous observez le système une fois qu'il s'est stabilisé.
Délais d'attente, nouvelles 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 tient actuellement grâce à time.Sleep(100 * time.Millisecond) et à l'espoir, voici votre sortie.
3.7. Les changements de net/http qui affectent réellement la production
Discrets, mais ils apparaîtront dans vos métriques.
Les corps de réponse HTTP/1 se vident automatiquement à la fermeture. Le contenu non lu est désormais vidé (jusqu'à une limite prudente) lors de la fermeture du corps, afin que la connexion puisse être réutilisée. Ce qui signifie que vous pouvez enfin supprimer cette formule que tout le monde a copiée-collée depuis la même réponse de Stack Overflow depuis 2016 :
1// désormais inutile
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Pour la plupart des programmes, c'est sans effet ou légèrement bénéfique. Si cela empire les choses, vous vous trouvez probablement dans le cas que les notes de version décrivent poliment : Transport.MaxIdleConns réglé à 0, ou un nouveau Client par requête, contournant entièrement la limite de connexions inactives. 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ériez l'ancien ordonnancement en tourniquet, Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Limite le nombre de valeurs d'en-tête que le serveur acceptera, avec pour valeur par défaut DefaultMaxHeaderValueCount. Une porte de plus fermée à l'option « envoyez dix mille en-têtes et voyez 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 la négociation TLS ALPN dessus, de sorte que les dialers personnalisés passant par un proxy peuvent toujours négocier HTTP/2.
3.8. Les petites nouveautés qui mettent de la joie
strings.CutLast / bytes.CutLast. Cut découpe au premier séparateur. Il n'existait pas de variante « dernier », donc chacun écrivait à la main LastIndex suivi d'un découpage, et environ 30 % d'entre nous ont eu un décalage d'un caractère du premier coup.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Parfait pour les extensions de fichiers et pour analyser host:port, où il faut trouver les deux-points finaux, car les adresses IPv6 en sont truffées.
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 inscrite dans un règlement, cela vous concerne.
url.URL.Clone et url.Values.Clone. Des copies profondes. L'ancienne copie superficielle *u partageait le pointeur Userinfo, ce qui produisait exactement le genre de bogue 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
Les méthodes de lecture de net.UnixConn renvoient désormais io.EOF directement au lieu de l'envelopper dans net.OpError. err == io.EOF fonctionne désormais, comme il aurait toujours dû le faire.
database/sql.ConvertAssign et driver.RowsColumnScanner. Fonctionnalités destinées aux auteurs de pilotes. La première expose les conversions de types effectuées par Rows.Scan ; la seconde permet aux pilotes de scanner directement dans les destinations de l'utilisateur, en évitant une allocation intermédiaire.
unicode 15 → 17. Deux versions d'un seul coup. 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 plus rapide, et ses octets de sortie peuvent différer de ceux de Go 1.26. Cela se répercute sur archive/zip, compress/gzip, compress/zlib et image/png. Si vous avez des tests de fichiers de référence qui hachent une sortie compressée, ils vont casser, et ce ne sera pas une régression. Vérifiez cela avant la mise à niveau, et non pendant le compte rendu d'incident.
4. Chaîne d'outils
4.1. go fix a reçu davantage de modernisateurs
go fix se transforme discrètement en outil de modernisation du 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 modernisateurs se sont déclenchés sur un petit fichier :
embedlit: réécrit les littéraux embarqués avec la nouvelle syntaxe de la §1.2atomictypes:atomic.AddInt64(&x, 1)devientatomic.Int64.Add(1), ce qui rend impossible l'accès non atomique et corrige discrètement des bogues d'alignement sur 32 bits que vous ne soupçonniez passlicesbackward: les boucles inversées deviennentslices.Backwardwaitgroupgo: le rituelAdd/go/Donedevientwg.Go(renommé depuiswaitgroupde la 1.26 pour éviter toute ambiguïté)
go tool fix help énumère les 26 modernisateurs ; go tool fix help <nom> explique l'un d'eux. Par ailleurs, fmtappendf a été supprimé « pour des raisons stylistiques », 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 constitue une après-midi véritablement satisfaisante. Lisez le diff d'abord, bien évidemment.
4.2. go test exécute stdversion par défaut
C'est le changement le plus susceptible de perturber votre journée.
go test exécute désormais par défaut la vérification vet stdversion, qui signale les symboles de la bibliothèque standard plus récents que ce que la directive go de votre go.mod autorise :
1// go.mod indique : go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast est un symbole de la 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 élimine le mode de défaillance 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 reçu 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 de t.Log (sans champ), celle de 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. Consultez la documentation d'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 à niveau.
-ex et affichage du code 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
Le code source de l'exemple et la 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 épars en deux au maximum (directs et indirects), 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 surtout après la résolution de conflits de fusion Git, où naissent les blocs require égarés. Si votre équipe compte plus de trois personnes ajoutant des dépendances, les conflits de votre go.mod vont devenir nettement plus silencieux.
4.6. Divers
- Prise en charge de
bzrsupprimée. Si cela vous concerne, j'aimerais sincèrement connaître votre 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'est ce que vous voulez. Cela s'aligne désormais surgo tool pprofet prévient l'occasionnel profileur accidentellement public.- 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, plateformes
Compilateur. Les noms de fichiers relatifs dans les directives //line sont désormais résolus par rapport au répertoire du fichier qui les contient, comme dans go/scanner. Pertinent si vous écrivez des générateurs de code.
Les noms de symboles des littéraux de fonctions (closures) sont également plus simples désormais : le même nom quel que soit l'inlining, et plusieurs instances d'un même littéral peuvent partager du code dans le binaire. Aucun changement fonctionnel, sauf que 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 la pratiquez, c'est désormais qu'elle commencera à mentir plus fort.
Éditeur de liens. Les nouvelles options -macos et -macsdk définissent les versions de l'OS et du SDK dans la commande de chargement LC_BUILD_VERSION de macOS.
Plateformes.
- Darwin nécessite désormais macOS 13 Ventura ou ultérieur, comme annoncé dans la 1.26. Vérifiez vos runners CI.
- PowerPC (
GOOS=linux GOARCH=ppc64) est passé à l'ABI ELFv2. Nécessite un noyau Linux 3.13+ (RHEL7 l'a rétroporté en 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 statique en Go pur, définissezCGO_ENABLED=0.
6. Liste de contrôle de la mise à niveau
Go 1.27 prend la compatibilité au sérieux, mais vérifiez les points suivants avant d'incrémenter la version :
- Tests de fichiers de référence sur la sortie compressée.
compress/flatea changé d'encodeurs ; les octets gzip/zip/png peuvent différer. - Tests comparant les chaînes de messages d'erreur JSON. Le comportement est identique ; le texte de l'erreur ne l'est pas.
- GODEBUG supprimés présents dans
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf: ils ne font échouer la compilation que si leur valeur est réglée sur leur ancienne valeur. - Violations de
stdversion.go testles détecte désormais. Exécutez-le tôt si vous maintenez une bibliothèque avec une directivegoconservatrice. - Runners CI sous macOS 12 ou antérieur. La prise en charge a disparu.
- Tests vérifiant les noms de symboles des littéraux de fonctions, et comparaisons de fonctions via
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0ou unClientpar requête. Combiné au vidage automatique des corps de réponse, cela peut ralentir les choses.
Réflexions finales
Go 1.27 est la version où une grande quantité 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 la 1.18. Les sélecteurs de champs dans les littéraux de structure ont clos un problème vieux de onze ans. encoding/json/v2 représente deux ans de discussions sur des propositions. Et uuid comble un vide que pratiquement tous les projets Go comblaient depuis une décennie avec la même dépendance tierce.
Si vous voulez un ordre de priorité pour tirer réellement parti de cette version :
httptest.NewTestServer+synctest.Sleep: les tests HTTP dépendant du temps deviennent rapides et déterministes. Le meilleur rapport effort/gain de toute la version.- Le profil de fuite de goroutines : exposez un point d'accès en préproduction et préparez-vous à être humilié.
- Étiquettes de goroutines dans les traces : trois lignes de
pprof.Doà votre point d'entrée de requêtes transforment votre prochain vidage de pile à 3 h du matin en quelque chose de lisible. go fix ./...: laissez l'outil effectuer la modernisation.encoding/json/v2: sans précipitation. Adoptez-le une option à la fois.
Procurez-vous go1.27rc3 et exécutez dès maintenant votre suite de tests avec. 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