---
id: 1a31e06c0d969b4b6cbbc590259cf089
author: iwanhae
title: Go 1.27, ou comment j'ai appris à cesser de m'inquiéter et à aimer les méthodes génériques
description: Méthodes génériques, encoding/json/v2, un package uuid standard et un détecteur de fuites de goroutines qui fonctionne réellement. Une présentation de Go 1.27 accompagnée d'exemples tous effectivement exécutés.
language: fr
date: 2026-08-15T02:08:43.129534158Z
path: /blog/posts/go-1-27-or-how-i-learned-to-stop-worrying-and-love-generic-methods-za800e454
category: release
---

![Vue d'ensemble de Go 1.27](/assets/images/whats-new-in-go1.27/overview.webp)

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é.

```bash
go install golang.org/dl/go1.27rc3@latest
go1.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 `Map` sur 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 :

```go
type List[E any] []E

// F est un paramètre de type déclaré par la méthode elle-même
func (l List[E]) Apply[F any](f func(E) F) List[F] {
	r := make(List[F], len(l))
	for i, x := range l {
		r[i] = f(x)
	}
	return r
}

func main() {
	l := List[int]{1, 2, 3}
	fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
	// [10 20 30]
}
```

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 :

```go
type Bag struct{ items []any }

func (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 :

1. Les méthodes d'interface ne peuvent pas déclarer de paramètres de type.
2. 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 :

```go
type Adder interface{ Add(int) int }

type G struct{}

func (G) Add[T any](v T) T { return v }

var _ Adder = G{}
```

```
cannot use G{} (value of struct type G) as Adder value in variable declaration:
	G does not implement Adder (wrong type for method Add)
		have Add[T any](T) T
		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 :

```go
func (r *Rand) N[Int intType](n Int) Int
```

```go
r := rand.New(rand.NewPCG(1, 2))
fmt.Println(r.N(int32(100)))     // 76
fmt.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](https://go.dev/issue/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.

```go
type Object struct{ name, color string }
type Point3D struct {
	Object
	x, y, z float64
}
type Line struct {
	Object
	p, q Point3D
}

// Go 1.27 : utiliser directement le champ promu
line := 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.

```go
_ = Line{Object.name: "x"}   // invalid field name Object.name in struct literal
_ = 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.**

```go
obj := Object{"edge", "black"}
_ = Line{Object: obj, name: "diagonal"}
// 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é.**

```go
type PtrEmbed struct {
	*Object
	z int
}

_ = PtrEmbed{name: "x"}
// 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é** :

```go
func double[T ~int | ~float64](v T) T { return v * 2 }

type S struct{ f func(int) int }
type A [1]func(int) int

func main() {
	s := S{f: double}          // 1.26 : nécessitait double[int]
	a := A{double}             // 1.26 : nécessitait double[int]

	c := make(chan func(int) int, 1)
	c <- double                // 1.26 : nécessitait double[int]

	var fn func(float64) float64 = double  // cela a toujours fonctionné

	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
	// 42 10 14 3
}
```

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 :

```go
type small struct{ a, b, c, d int }

var sink any

func BenchmarkAlloc(b *testing.B) {
	for b.Loop() {
		sink = &small{}
	}
}

func BenchmarkSlice(b *testing.B) {
	for b.Loop() {
		sink = make([]byte, 48)
	}
}
```

```
# par défaut (malloc spécialisé par taille activé)
BenchmarkAlloc-12     3000000     5.358 ns/op
BenchmarkSlice-12     3000000    13.58 ns/op

# GOEXPERIMENT=nosizespecializedmalloc
BenchmarkAlloc-12     3000000     8.722 ns/op
BenchmarkSlice-12     3000000    20.81 ns/op

```

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 :

```
2413202 bytes  (par défaut)
2361826 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.

```go
func leak() {
	ch := make(chan int) // personne ne enverra jamais rien. personne.
	go func() {
		<-ch
	}()
}

func main() {
	for range 3 {
		leak()
	}
	runtime.GC()
	runtime.GC()
	pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
}
```

```
goroutineleak profile: total 3
3 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
#	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`.

```go
func handle(ctx context.Context) {
	var wg sync.WaitGroup
	wg.Go(func() {
		buf := make([]byte, 4<<10)
		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
	})
	wg.Wait()
}

func main() {
	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
	pprof.Do(context.Background(), labels, handle)
}
```

```
goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
main.handle.func1()
	/tmp/go127/tblabel/main.go:15 +0x40
sync.(*WaitGroup).Go.func1()
	...
goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
...
```

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 :

```
# go.mod contient : godebug asynctimerchan=1
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)

# go.mod contient : godebug asynctimerchan=0
(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](https://go.dev/issue/71497) 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 :

```go
// encoding/json.Unmarshal de Go 1.27, résumé
func Unmarshal(data []byte, v any) error {
	return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
}
```

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 :

```go
var m map[string]int
json.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 :**

```go
var m map[string]int
jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
// jsontext: duplicate object member name "a"

var s string
jsonv2.Unmarshal([]byte("\"\xff\""), &s)
// 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 :

```go
type Config struct {
	Name    string   `json:"name"`
	Tags    []string `json:"tags,omitzero"`
	Timeout int      `json:"timeout,omitzero"`
}

out, _ := jsonv2.Marshal(Config{Name: "api"})
// {"name":"api"}

// clés de map triées + indentation
out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
	jsonv2.Deterministic(true), jsontext.WithIndent("  "))
// {
//   "a": 1,
//   "b": 2
// }

// rejeter les champs inconnus : nécessitait auparavant un Decoder et DisallowUnknownFields()
var c Config
err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
	jsonv2.RejectUnknownMembers(true))
// json: cannot unmarshal JSON string into Go main.Config:
//   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 :

```go
// conserver la sémantique v1, mais rejeter les clés dupliquées comme le fait la v2
jsonv2.Unmarshal(data, &v,
	json.DefaultOptionsV1(),
	jsontext.AllowDuplicateNames(false))
// 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`.

```go
import "uuid"

func main() {
	fmt.Println(uuid.NewV4())
	// b97aa695-da08-472c-af81-ff088129019f

	// v7 : les 48 bits de poids fort sont un horodatage, ils se trient donc toujours dans l'ordre de création
	a, b := uuid.NewV7(), uuid.NewV7()
	fmt.Println(a.Compare(b) < 0) // true

	u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
	fmt.Println(u, err)
	// 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>

	fmt.Println(uuid.Nil(), uuid.Max())
	// 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
}
```

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 :

- **`UUID` est un `[16]byte`**, donc `==` fonctionne et il peut être utilisé directement comme clé de map. Même conception que `google/uuid`.
- **`Nil` et `Max` sont des fonctions, et non des variables.** Car une `var Nil UUID` au 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.
- **`NewV7` est 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.

```go
sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
pk := sk.PublicKey()

msg := []byte("release the gophers")
opts := &mldsa.Options{Context: "gosuda.org/blog"}

sig, _ := sk.Sign(rand.Reader, msg, opts)

fmt.Println("private key seed:", len(sk.Bytes()), "bytes")  // 32
fmt.Println("public key:", len(pk.Bytes()), "bytes")        // 1952
fmt.Println("signature:", len(sig), "bytes")                // 3309

fmt.Println(mldsa.Verify(pk, msg, sig, opts))
// <nil>
fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
// 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`.

```go
// dst = a*x + y
func axpy(dst, x, y []float32, a float32) {
	va := simd.BroadcastFloat32s(a)
	w := va.Len()

	i := 0
	for ; i+w <= len(x); i += w {
		vx := simd.LoadFloat32s(x[i:])
		vy := simd.LoadFloat32s(y[i:])
		vx.MulAdd(va, vy).Store(dst[i:])
	}
	// traiter le reste avec un chargement/stockage partiel : pas de boucle scalaire de nettoyage
	if i < len(x) {
		vx, _ := simd.LoadFloat32sPart(x[i:])
		vy, _ := simd.LoadFloat32sPart(y[i:])
		vx.MulAdd(va, vy).StorePart(dst[i:])
	}
}
```

```bash
$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
vector bits: 128 emulated: false
[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 :

```go
type Hasher[T any] interface {
	Hash(*Hash, T)
	Equal(x, y T) bool
}
```

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 :

```go
type CaseInsensitive struct{}

func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
	h.WriteString(strings.ToLower(s))
}
func (CaseInsensitive) Equal(x, y string) bool {
	return strings.ToLower(x) == strings.ToLower(y)
}

var seed = maphash.MakeSeed()

func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
	var h maphash.Hash
	h.SetSeed(seed)   // même graine, sinon tout cela n'a aucun sens
	hr.Hash(&h, v)
	return h.Sum64()
}

fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
// true
```

Pour la sémantique ordinaire de `==`, il existe `ComparableHasher[T]` :

```go
hashOf(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 :

```go
func TestRetryWithBackoff(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		var hits int
		srv := httptest.NewTestServer(t, http.HandlerFunc(
			func(w http.ResponseWriter, r *http.Request) {
				hits++
				if hits < 3 {
					w.WriteHeader(http.StatusServiceUnavailable)
					return
				}
				io.WriteString(w, "ok")
			}))

		client := srv.Client()
		start := time.Now()
		var body string
		for attempt := range 5 {
			resp, err := client.Get(srv.URL)
			if err != nil {
				t.Fatal(err)
			}
			b, _ := io.ReadAll(resp.Body)
			resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				body = string(b)
				break
			}
			synctest.Sleep(time.Duration(1<<attempt) * time.Second)
		}

		if body != "ok" {
			t.Fatalf("got %q", body)
		}
		// le backoff doit être exactement 1s + 2s = 3s
		if elapsed := time.Since(start); elapsed != 3*time.Second {
			t.Fatalf("elapsed = %v, want 3s", elapsed)
		}
		t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
	})
}
```

```
=== RUN   TestRetryWithBackoff
    x_test.go:48: hits=3 elapsed=3s
--- 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 :

```go
// désormais inutile
defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()
```

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.

```go
name, ext, ok := strings.CutLast("archive.tar.gz", ".")
// 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 ? » :

```go
x, y := big.NewInt(-7), big.NewInt(2)

new(big.Int).Divide(x, y, new(big.Int), big.Trunc)  // q=-3 r=-1
new(big.Int).Divide(x, y, new(big.Int), big.Floor)  // q=-4 r=1
new(big.Int).Divide(x, y, new(big.Int), big.Round)  // q=-4 r=1
new(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.

```go
orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
clone := orig.Clone()
clone.Host = "example.com"
fmt.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 :

```go
package fixdemo

import (
	"sync"
	"sync/atomic"
)

type Base struct{ ID int }
type User struct {
	Base
	Name string
}

func New() User {
	return User{Base: Base{ID: 1}, Name: "gopher"}
}

var counter int64

func Incr() { atomic.AddInt64(&counter, 1) }

func Reverse(s []string) {
	for i := len(s) - 1; i >= 0; i-- {
		_ = s[i]
	}
}

func Spawn(wg *sync.WaitGroup) {
	wg.Add(1)
	go func() {
		defer wg.Done()
	}()
}
```

```bash
$ go1.27rc3 fix -diff ./fixdemo
```

```diff
 import (
+	"slices"
 	"sync"
 	"sync/atomic"
 )

 func New() User {
-	return User{Base: Base{ID: 1}, Name: "gopher"}
+	return User{ID: 1, Name: "gopher"}
 }

-var counter int64
+var counter atomic.Int64

-func Incr() { atomic.AddInt64(&counter, 1) }
+func Incr() { counter.Add(1) }

 func Reverse(s []string) {
-	for i := len(s) - 1; i >= 0; i-- {
-		_ = s[i]
+	for _, v := range slices.Backward(s) {
+		_ = v
 	}
 }

 func Spawn(wg *sync.WaitGroup) {
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-	}()
+	wg.Go(func() {
+	})
 }
```

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.2
- `atomictypes` : `atomic.AddInt64(&x, 1)` devient `atomic.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 pas
- `slicesbackward` : les boucles inversées deviennent `slices.Backward`
- `waitgroupgo` : le rituel `Add`/`go`/`Done` devient `wg.Go` (renommé depuis `waitgroup` de 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 :

```go
// go.mod indique : go 1.24
package sv

import "strings"

func Ext(name string) string {
	_, ext, _ := strings.CutLast(name, ".")  // CutLast est un symbole de la 1.27
	return ext
}
```

```
$ go1.27rc3 test ./...
# sv
./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
FAIL	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"`.

```
'frame'  '=== RUN   TestFail\n'
None     '    x_test.go:6: hello\n'
'error'  '    x_test.go:7: boom\n'
'frame'  '--- FAIL: TestFail (0.00s)\n'
'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 :

```bash
$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
package errgroup // import "golang.org/x/sync/errgroup"
...
```

Pratique pour vérifier ce qui a changé avant une mise à niveau.

**`-ex` et affichage du code source des exemples :**

```bash
$ go1.27rc3 doc -ex strings | grep Example
    func ExampleClone()
    func ExampleCompare()
    func ExampleContains()
    func ExampleCut()
    func ExampleCutPrefix()
    ...

$ go1.27rc3 doc strings.ExampleCut
package main

import (
	"fmt"
	"strings"
)

func main() {
	show := func(s, sep string) {
		before, after, found := strings.Cut(s, sep)
		fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
	}
	show("Gopher", "Go")
	...
}

Output:
Cut("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.

```go
// Avant
module tidydemo

go 1.27

require golang.org/x/sync v0.10.0

// networking
require golang.org/x/net v0.33.0

require (
	golang.org/x/sys v0.28.0 // indirect
)

require golang.org/x/text v0.21.0 // indirect
```

```go
// Après : go mod tidy
module tidydemo

go 1.27

require (
	// networking
	golang.org/x/net v0.33.0
	golang.org/x/sync v0.10.0
)
```

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 `bzr` supprimée.** Si cela vous concerne, j'aimerais sincèrement connaître votre histoire.
- **`go tool trace -http` se lie à localhost** lorsqu'on ne lui donne qu'un port. `-http=:6060` n'écoute plus sur toutes les interfaces. Utilisez `-http=0.0.0.0:6060` si c'est ce que vous voulez. Cela s'aligne désormais sur `go tool pprof` et prévient l'occasionnel profileur accidentellement public.
- **Les fichiers de réponse (`@file`)** sont pris en charge par `compile`, `link`, `asm`, `cgo`, `cover` et `pack`, 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éfinissez `CGO_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/flate` a 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 test` les détecte désormais. Exécutez-le tôt si vous maintenez une bibliothèque avec une directive `go` conservatrice.
- [ ] **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 = 0`** ou un `Client` par 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 :

1. **`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.
2. **Le profil de fuite de goroutines** : exposez un point d'accès en préproduction et préparez-vous à être humilié.
3. **É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.
4. **`go fix ./...`** : laissez l'outil effectuer la modernisation.
5. **`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**

- [Notes de version de Go 1.27](https://go.dev/doc/go1.27)
- [La spécification du langage de programmation Go](https://go.dev/ref/spec)
- [proposal: encoding/json/v2 (#71497)](https://go.dev/issue/71497)
- [proposal: portable SIMD (#78902)](https://go.dev/issue/78902)
- [proposal: hash/maphash.Hasher (#70471)](https://go.dev/issue/70471)
