Retour au blog

Nouveautés de Go 1.27 : le guide complet

Go 1.27 apporte les méthodes génériques, un package uuid dans la bibliothèque standard et json/v2 comme moteur par défaut. Voici ce qu'il faut adopter tout de suite, et ce qui casse sans bruit lors de la mise à jour.

Nouveautés de Go 1.27 : le guide complet

Go 1.27 ajoute cinq nouveaux packages à la bibliothèque standard (encoding/json/v2, encoding/json/jsontext, crypto/mldsa, uuid et simd) ainsi que trois changements du langage. Certains vous serviront dès le premier jour, comme les méthodes génériques et un package uuid qui retire une dépendance de la plupart des fichiers go.mod. D'autres feront échouer votre suite de tests avant même que vous ayez remarqué la fonctionnalité : compress/flate produit des octets différents, les littéraux de fonction reçoivent des noms de symboles différents, et une poignée de GODEBUG qui servaient de porte de sortie font désormais échouer le build au lieu de restaurer l'ancien comportement.

Table des matières

Les méthodes génériques, et la seule chose qu'elles ne savent toujours pas faire

Depuis l'arrivée des génériques dans Go 1.18, les paramètres de type ne fonctionnaient qu'à deux endroits : les fonctions de premier niveau et les déclarations de type. Les méthodes en étaient exclues. Si vous vouliez une transformation qui change le type des éléments de votre collection, vous deviez donc l'écrire sous forme de fonction au niveau du package, et les fonctions au niveau du package ne s'enchaînent pas.

Vous avez sans doute déjà écrit quelque chose de ce genre. Un pipeline de métriques qui transforme des relevés de capteurs en libellés d'affichage a besoin de deux transformations, et chacune doit envelopper l'appel précédent :

example.gogo
type Metric struct {
    Sensor  string
    Celsius int
}

type List[T any] []T

// Package-level, because a method couldn't declare U.
func Map[T, U any](l List[T], f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    temps := Map(metrics, func(m Metric) int { return m.Celsius })
    labels := Map(temps, func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

Avec Go 1.27, Map devient une vraie méthode qui déclare son propre paramètre de type, indépendamment du récepteur :

example.gogo
// U is the method's own type parameter, new in Go 1.27.
func (l List[T]) Map[U any](f func(T) U) List[U] {
    out := make(List[U], len(l))
    for i, v := range l {
        out[i] = f(v)
    }
    return out
}

func main() {
    metrics := List[Metric]{
        {Sensor: "cpu0", Celsius: 61},
        {Sensor: "cpu1", Celsius: 58},
    }

    labels := metrics.
        Map(func(m Metric) int { return m.Celsius }).
        Map(func(c int) string { return fmt.Sprintf("%dC", c) })

    fmt.Println(labels) // [61C 58C]
}

L'enchaînement est l'amélioration évidente. Le nommage l'est moins : MapSlice, MapSet et MapBox se réduisent à un seul Map par type, car le récepteur indique déjà de quel type il s'agit. Le récepteur aide aussi le compilateur : List[Metric] fixe T, il ne reste donc que le type de sortie à inférer. Enfin, les méthodes apparaissent quand vous tapez un point dans votre éditeur. Un helper au niveau du package ne vous sert que si vous savez déjà qu'il existe.

La bibliothèque standard utilise elle-même la fonctionnalité. math/rand/v2 déclare désormais (*Rand) N[Int intType](Int) Int comme méthode, à côté de la fonction N au niveau du package, ce que les anciennes règles n'autorisaient pas.

Il y a une limite, et elle passe facilement inaperçue. D'après les release notes :

Ceci ne compile donc pas :

example.gogo
type Transformer interface {
    // Compile error: interface methods can't declare type parameters.
    Map[U any](f func(Metric) U) any
}

Le dispatch des interfaces a lieu à l'exécution, le compilateur ne peut donc pas savoir quelles instanciations d'une méthode générique générer pour un appel résolu dynamiquement. Si votre API repose sur une interface, les transformations ont toujours besoin de l'ancienne fonction au niveau du package. L'intérêt des méthodes génériques pour votre codebase se joue surtout là, alors vérifiez vos interfaces avant de planifier un refactoring. Si vous voulez vous entraîner, le cours Go Generics Masterclass couvre les contraintes et l'inférence sous forme d'exercices dans le navigateur.

Verdict : à adopter dès maintenant, pour les types concrets. Laissez de côté les API construites autour d'interfaces.

Un package uuid dans la bibliothèque standard

Presque tous les services Go qui dialoguent avec une base de données importent github.com/google/uuid. Go 1.27 ajoute un package de la bibliothèque standard dont le chemin d'import est "uuid", à la suite de la proposition #62026.

UUID est défini comme [16]byte, ce qui signifie que les valeurs sont comparables avec == et utilisables directement comme clés de map. Trois générateurs sont fournis :

example.gogo
package main

import (
    "fmt"
    "uuid"
)

func main() {
    fmt.Println(uuid.New())   // reach for this when you don't care how it's made
    fmt.Println(uuid.NewV4()) // 128 bits, 122 of them random
    fmt.Println(uuid.NewV7()) // 48-bit timestamp first, so values sort by creation time

    requestID, err := uuid.Parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
    if err != nil {
        return
    }
    fmt.Println(requestID, requestID == uuid.Nil()) // false, it parsed fine
}

New est le générateur par défaut et renvoie actuellement un V4. NewV4 est entièrement aléatoire, personne ne peut donc deviner le suivant. C'est ce qu'il vous faut pour un identifiant de requête public. NewV7 place un horodatage de 48 bits en tête, si bien que les valeurs récentes se trient après les plus anciennes. Elles font donc de bonnes clés primaires, car les insertions arrivent vers la fin de l'index au lieu de s'éparpiller partout.

Notez que Nil() et Max() sont des fonctions : la comparaison s'écrit donc id == uuid.Nil(), avec les parenthèses. Nil() sert de sentinelle « pas encore défini ». Max() est la valeur dont tous les bits valent 1, utile comme borne supérieure quand vous parcourez une plage de clés V7 ordonnées dans le temps.

Verdict : à adopter dès maintenant pour le nouveau code. Migrer un service existant se résume à un rechercher-remplacer, mais consultez la FAQ avant de supprimer la dépendance.

Faut-il modifier votre code pour encoding/json/v2 ?

Non. C'était tout l'objectif de la conception. encoding/json s'appuie désormais sur l'implémentation v2, et les release notes le disent explicitement : « le comportement du marshaling et de l'unmarshaling est préservé, mais le texte exact des messages d'erreur peut différer. » Vous profitez de la vitesse du nouveau moteur sans modifier une ligne de code. Selon les release notes, « les performances de Marshal sont globalement au niveau de l'implémentation précédente, tandis que celles de l'unmarshaling sont nettement meilleures. »

Vous n'optez pour le nouveau comportement qu'en important encoding/json/v2 nommément. Ce package a des valeurs par défaut plus strictes, choisies pour correspondre à ce qu'attendent les autres implémentations JSON : il rejette l'UTF-8 invalide dans les chaînes JSON, rejette les noms en double au sein d'un objet JSON, compare les noms de champs en respectant la casse, sérialise une slice nil en [] plutôt qu'en null, et sérialise les maps dans un ordre non déterministe là où v1 garantissait un ordre déterministe. Le changement sur les slices nil correspond à la distinction entre nil et vide présentée dans var vs make en Go, et avec v2, vous la voyez dans la sortie.

example.gogo
import "encoding/json/v2"

type Event struct {
    ID     string `json:"id"`
    Action string `json:"action"`
}

// v1 would match the key "ID" against the tag `json:"id"`. v2 won't,
// and it skips the mismatched key silently rather than erroring.
var e Event
json.Unmarshal([]byte(`{"ID":"evt_01H","action":"checkout"}`), &e)
fmt.Println(e.ID, e.Action) // "" checkout

// v2 marshals maps in a non-deterministic order. Ask for a stable one
// explicitly when you hash or snapshot the output.
inventory := map[string]int{"widget": 12, "gadget": 3, "gizmo": 7}
b, _ := json.Marshal(inventory, json.Deterministic(true))

Pour retrouver un comportement de v1 sous v2, vous ne changez pas d'import. Vous passez une option pour ce comportement précis : Deterministic(true) pour un ordre stable des maps, MatchCaseInsensitiveNames(true) pour une correspondance souple des champs, FormatNilSliceAsNull(true) pour null au lieu de []. Le package v1 a reçu les mêmes options, vous pouvez donc adopter la sémantique v2 un comportement à la fois, sans migration complète. La liste complète se trouve dans la section Migrating to v2 de la documentation du package v1.

Le troisième package, encoding/json/jsontext, gère la syntaxe de plus bas niveau. Il expose le JSON comme une suite de tokens et de valeurs, avec une machine à états qui garantit la validité de la sortie. Les codecs en streaming s'appuient dessus. La plupart du code applicatif ne l'importe jamais directement.

Pour choisir lequel importer :

flowchart TD
    A["Which JSON import?"] --> B{"Upgrading existing code?"}
    B -- Yes --> C["encoding/json<br/>v2 engine, v1 behavior"]
    B -- No --> D{"Want strict defaults<br/>and faster decoding?"}
    D -- No --> C
    D -- Yes --> E["encoding/json/v2"]
    E --> F{"Need raw tokens<br/>or streaming syntax?"}
    F -- Yes --> G["encoding/json/jsontext"]
    F -- No --> H["You're done"]

Arbre de décision pour choisir entre encoding/json, encoding/json/v2 et encoding/json/jsontext dans Go 1.27.

Verdict : adoptez le moteur par défaut dès maintenant (c'est déjà fait), et v2 nommément plus tard, une fois que vous aurez lu les notes de migration concernant vos struct tags. Le cours Data Formats couvre les règles de tags qui déterminent à quel point cela vous concerne.

Signatures post-quantiques avec crypto/mldsa

RSA et ECDSA sont sûrs aujourd'hui parce qu'aucun ordinateur ne sait factoriser de grands nombres ou calculer des logarithmes discrets assez vite. Un ordinateur quantique suffisamment puissant le pourrait. crypto/mldsa implémente ML-DSA, défini par FIPS 204, avec trois jeux de paramètres, 44, 65 et 87, à la suite de la proposition #77626.

D'autres packages le prennent déjà en charge. crypto/x509 analyse et vérifie les clés et signatures ML-DSA, et crypto/tls accepte les trois jeux de paramètres lors d'un handshake TLS 1.3. MLKEM1024 rejoint les échanges de clés pris en charge pour un accord de clé résistant au quantique, activé en l'ajoutant à Config.CurvePreferences.

Une signature et une vérification complètes pour une release de firmware :

example.gogo
sk, err := mldsa.GenerateKey(mldsa.MLDSA65())
if err != nil {
    return err
}

firmware, err := os.ReadFile("firmware-v2.1.0.bin")
if err != nil {
    return err
}

// Context is a domain-separation label. Sign and verify must pass the same one.
opts := &mldsa.Options{Context: "acme/firmware-release"}
sig, err := sk.Sign(nil, firmware, opts)
if err != nil {
    return err
}

fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // true
fmt.Println(mldsa.MLDSA65().SignatureSize())                         // 3309

firmware[0] ^= 1 // flip one bit
fmt.Println(mldsa.Verify(sk.PublicKey(), firmware, sig, opts) == nil) // false

Verify renvoie nil quand la signature est valide, la comparaison avec nil vous donne donc un booléen. L'inconvénient, c'est la taille, et l'écart est important :

Graphique à barres horizontales comparant la taille des signatures numériques en octets. Ed25519 fait 64 octets, ECDSA P-256 environ 71 octets, RSA-2048 256 octets, ML-DSA-44 2 420 octets, ML-DSA-65 3 309 octets et ML-DSA-87 4 627 octets.

Source : tailles de signature selon FIPS 204 (ML-DSA) et les spécifications des algorithmes respectifs.

Une signature ML-DSA-65 fait environ 46 fois la taille d'une signature Ed25519, elle ne vaut donc la peine que là où la signature doit durer. Le firmware et les artefacts de release auxquels un appareil devra encore faire confiance dans dix ans valent bien 3 KB. Il en va de même pour les racines de certificats censées survivre à la cryptographie actuelle, et pour les serveurs TLS dont vous préférez ne pas avoir à changer les clés une fois les ordinateurs quantiques arrivés. Pour des jetons de session à courte durée de vie que vous émettez par milliers chaque seconde, la taille supplémentaire est un surcoût dont vous n'avez pas encore besoin.

Un changement sans rapport dans crypto/x509 touche le même domaine. SystemCertPool respecte désormais SSL_CERT_FILE et SSL_CERT_DIR sous Windows et Darwin, et plus seulement sous Linux. Quand ces variables sont définies, Go charge les racines depuis le disque et utilise son propre vérificateur au lieu des API de la plateforme. Définissez GODEBUG=x509sslcertoverrideplatform=0 pour conserver l'ancien comportement.

Verdict : à adopter pour les signatures à longue durée de vie, à ignorer pour tout le reste.

Le package expérimental simd

Une instruction CPU classique travaille sur une seule valeur. Une instruction SIMD travaille sur tout un vecteur de valeurs en une seule étape, si bien que le même résultat demande beaucoup moins d'instructions. C'est utile quand vous appliquez les mêmes calculs à une grande slice d'échantillons audio ou de pixels d'image, ou quand vous calculez un produit scalaire. Le code qui se contente surtout de déplacer des structs et des chaînes n'en profitera pas.

Chaque architecture expose SIMD différemment. Le nouveau package simd est portable et ne présuppose aucune taille de vecteur. Il est disponible sur toutes les architectures et utilise les instructions matérielles là où elles existent. Activez-le avec GOEXPERIMENT=simd au moment du build. Il est expérimental, l'API peut donc changer.

Mixer deux pistes audio, plusieurs échantillons par étape :

example.gogo
//go:build goexperiment.simd

// out[i] = trackA[i] + trackB[i], several samples at a time.
out := make([]float32, len(trackA))

lanes := simd.LoadFloat32s(trackA).Len() // how many float32s fit in one vector
for i := 0; i+lanes <= len(trackA); i += lanes {
    va := simd.LoadFloat32s(trackA[i:])
    vb := simd.LoadFloat32s(trackB[i:])
    va.Add(vb).Store(out[i:])
}
// A plain scalar loop handles the leftover tail.

Len indique combien d'éléments tiennent dans un vecteur sur la machine où le code s'exécute, la boucle n'a donc jamais besoin de coder en dur une largeur de vecteur. Chaque étape charge autant d'échantillons depuis les deux pistes, les additionne élément par élément en une seule opération, puis stocke le résultat.

Verdict : à ignorer pour l'instant, sauf si vous profilez déjà une boucle numérique critique. L'API est derrière un GOEXPERIMENT et susceptible de changer.

Les performances gagnées par simple mise à jour

Certaines accélérations ne demandent rien d'autre qu'une recompilation. La plus importante est l'allocation spécialisée par taille, que les release notes décrivent ainsi :

Graphique présentant l'allocateur spécialisé par taille de Go 1.27. Les allocations de moins de 80 octets deviennent jusqu'à 30 % moins coûteuses, les vrais programmes gourmands en allocations gagnent environ 1 % au total, et la taille du binaire augmente d'une quantité fixe de 60 kilo-octets.

Source : release notes de Go 1.27, allocation mémoire plus rapide.

Ces deux chiffres mesurent des choses différentes. Les 30 % correspondent au coût d'un seul petit appel d'allocation. Le ~1 % est ce que constate un service entier. Si vous rencontrez une régression, GOEXPERIMENT=nosizespecializedmalloc permet de désactiver la fonctionnalité, et cette porte de sortie devrait disparaître dans Go 1.28.

Trois optimisations du compilateur sont aussi activées par défaut. Une passe d'analyse de flot de données sur les bits connus suit quels bits d'une valeur valent de façon prouvable 0 ou 1, et élimine la redondance qui en résulte. Le déplacement de code invariant de boucle sort du corps de la boucle les calculs dont le résultat ne change jamais, qui s'exécutent alors une seule fois au lieu de s'exécuter à chaque itération. Enfin, les instructions switch se compilent désormais en tables de correspondance quand les cas le permettent, y compris avec fallthrough, et sautent directement au cas correspondant au lieu de tester chacun d'eux.

compress/flate devient aussi plus rapide, avec une réserve : la sortie encodée exacte peut différer de celle de Go 1.26. DEFLATE se trouve sous archive/zip, compress/gzip, compress/zlib et image/png, donc les tests golden à l'octet près sur n'importe lequel d'entre eux peuvent échouer. La sortie reste correcte, il suffit de régénérer les fixtures.

Verdict : à adopter dès maintenant. Vous en bénéficiez de toute façon en mettant à jour.

Détecter les fuites de goroutines et lire les tracebacks avec labels

Le profil goroutineleak est passé du statut d'expérimentation à la disponibilité générale, et le GOEXPERIMENT goroutineleakprofile est supprimé. Il est exposé via runtime/pprof et via l'endpoint /debug/pprof/goroutineleak de net/http/pprof.

Une goroutine qui fuit est une goroutine bloquée sur une primitive de concurrence qui ne pourra jamais se débloquer. Le runtime les détecte grâce au garbage collector : si la goroutine G est bloquée sur la primitive P, et que P n'est accessible depuis aucune goroutine exécutable ni depuis quoi que ce soit que celles-ci pourraient débloquer, alors G ne pourra jamais se réveiller.

example.gogo
func startJob() {
    result := make(chan int) // unbuffered: the send waits for a receiver
    go func() {
        result <- expensiveWork() // blocks forever, nobody receives
    }()
    // returns without receiving, so result becomes unreachable
}

func main() {
    startJob()
    runtime.GC() // the detector scans during a GC cycle
    pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
    // goroutineleak profile: total 1
    //   ... main.startJob.func1 ... main.go:14
}

Cette approche a un angle mort. Le runtime peut manquer des fuites lorsque la primitive bloquante est accessible via une variable globale ou via les variables locales d'une goroutine exécutable. Un channel stocké dans un registre au niveau du package compte comme accessible, le profil ne le signalera donc pas. Le cours Concurrency Fundamentals passe en revue les schémas de fuite qui produisent ces situations, et la manière de les corriger avec context.

La seconde amélioration du débogage ne demande aucun code. Pour les modules dont la directive go est 1.27 ou ultérieure, les tracebacks incluent désormais les labels de goroutine de runtime/pprof dans la ligne d'en-tête. Les labels que vous définissez déjà pour le profilage apparaissent dans les crash dumps, les traces SIGQUIT et la sortie de runtime.Stack. Quand deux goroutines ont des piles identiques, le label est souvent le seul moyen de les distinguer :

example.gogo
pprof.Do(ctx, pprof.Labels("request", id), func(ctx context.Context) {
    work()
})

// goroutine 34 [running]:
// labels: {"request":"req-42"}
// main.handle.func1 ...

Définissez GODEBUG=tracebacklabels=0 pour désactiver ce comportement si vos labels contiennent des informations que vous ne voulez pas voir dans un crash dump.

Verdict : à adopter dès maintenant. Ni l'un ni l'autre ne vous coûte quoi que ce soit.

Petites améliorations du langage et de la bibliothèque

Sélecteurs de champs dans les littéraux de struct

Une clé dans un littéral de struct peut désormais être n'importe quel sélecteur de champ valide pour le type, et plus seulement un nom de champ de premier niveau (proposition #9859). Si vous embarquez un modèle partagé, vous n'avez plus besoin d'écrire le littéral imbriqué en entier :

example.gogo
type Model struct {
    ID        int64
    CreatedAt time.Time
}

type Post struct {
    Model
    Author string
    Likes  int
}

p := Post{Model: Model{ID: 42}, Author: "Patrik"} // before
p := Post{ID: 42, Author: "Patrik"}               // Go 1.27

Inférence généralisée du type des fonctions

L'inférence du type des fonctions s'applique désormais dans tous les contextes où une fonction générique est affectée ou convertie vers un type de fonction correspondant (proposition #77245). Auparavant, elle ne fonctionnait que dans une déclaration de variable typée :

example.gogo
func ascending[T cmp.Ordered](a, b T) int  { return cmp.Compare(a, b) }
func descending[T cmp.Ordered](a, b T) int { return cmp.Compare(b, a) }

orders := []func(int, int) int{ascending[int], descending[int]} // before
orders := []func(int, int) int{ascending, descending}           // Go 1.27

Le type des éléments de la slice est func(int, int) int, Go infère donc T comme int. Cela fonctionne désormais aussi pour les conversions et pour le passage d'une fonction générique non instanciée en argument.

strings.CutLast et bytes.CutLast

Cut découpe sur le premier séparateur. CutLast découpe sur le dernier, et renvoie la partie avant, la partie après et un indicateur précisant si le séparateur a été trouvé (proposition #71151) :

example.gogo
dir, file, found := strings.CutLast("internal/store/user.go", "/")
fmt.Println(dir, file, found) // internal/store user.go true

Auparavant, il fallait appeler LastIndex, vérifier -1, puis découper deux fois à la main.

Rand.N, maphash.Hasher et big.Int.Divide

math/rand/v2 reçoit Rand.N sous forme de méthode générique (proposition #77853). Vous pouvez donc tirer une valeur aléatoire bornée depuis une source que vous avez initialisée avec une graine, plutôt que depuis la source globale, dont la graine ne peut pas être fixée. Un test en échec se rejoue alors avec les mêmes entrées.

hash/maphash gagne une interface générique Hasher[T] pour les structures basées sur le hachage, comme les maps personnalisées et les filtres de Bloom (proposition #70471). Elle associe une fonction de hachage à un test d'égalité, et des valeurs égales doivent produire le même hash. ComparableHasher[T] est l'implémentation prête à l'emploi pour les types comparables, qui compare avec ==. go/types l'utilise déjà et fournit un Hasher qui respecte la relation Identical.

math/big.Int reçoit une méthode Divide qui calcule le quotient et le reste en une fois, selon le mode d'arrondi de votre choix : Trunc, Floor, Round ou Ceil. Quo et Mod tronquent toujours vers zéro, le code financier et numérique qui a besoin d'un autre arrondi dispose donc désormais d'une option intégrée.

Unicode 17, database/sql et le reste

Le package unicode et tout ce qui repose dessus sont passés directement d'Unicode 15 à Unicode 17, les caractères ajoutés dans les deux versions intermédiaires sont donc désormais correctement classés. database/sql a gagné ConvertAssign, qui permet aux drivers de réutiliser les conversions de type effectuées par Rows.Scan au lieu d'écrire les leurs.

Le reste est mineur :

  • crypto ajoute la valeur de hachage MLDSAMu, un mécanisme de signalisation pour la signature ML-DSA avec mu externe.
  • crypto/ecdsa vérifie désormais que la longueur du hash est correcte dans PrivateKey.Sign lorsque vous passez des SignerOpts non nil.
  • crypto/x509 expose RawSignatureAlgorithm sur Certificate, CertificateRequest et RevocationList, ce qui vous donne l'AlgorithmIdentifier encodé en DER même quand SignatureAlgorithm vaut UnknownSignatureAlgorithm. L'analyse vers pkix.Name accepte aussi un éventail plus large de types de valeurs, les types inconnus atterrissant dans asn1.RawValue.
  • crypto/tls ajoute ConnectionState.LocalCertificate, la chaîne que vous avez présentée au pair, ainsi que QUICConfig.ClientHelloInfoConn. Config.Rand est déprécié au profit de testing/cryptotest.SetGlobalRandom pour les tests déterministes.
  • net fait en sorte que les méthodes de lecture de UnixConn renvoient directement io.EOF au lieu de l'envelopper dans un net.OpError.
  • runtime/secret propage le mode secret aux goroutines créées à l'intérieur.
  • go/constant ajoute StringLen, go/scanner ajoute Scanner.End, et go/token dote File d'une méthode String.
  • Côté portages, le portage PowerPC 64 bits big-endian sous Linux passe à l'ABI système ELFv2, ce qui y débloque cgo, les exécutables indépendants de la position et l'édition de liens externe, et nécessite un noyau Linux 3.13 ou ultérieur. Sous Plan 9, syscall.Errno est désormais défini et implémente error, le code portable qui y fait référence se compile donc sans contraintes de build. Le linker accepte aussi -macos et -macsdk pour définir les versions écrites dans la commande de chargement macOS LC_BUILD_VERSION.

Qu'est-ce qui change dans la toolchain Go ?

go fix a gagné quatre modernizers. embedlit simplifie les références aux champs embarqués dans les littéraux composites, ce que la nouvelle règle sur les littéraux de struct rend possible. atomictypes remplace les types de base dans les appels à sync/atomic par des types atomiques, slicesbackward réécrit les boucles à rebours avec slices.Backward, et unsafefuncs remplace l'arithmétique de pointeurs unsafe par des appels de fonction. Deux changements d'entretien les accompagnent : fmtappendf a été supprimé pour des raisons de style, et waitgroup a été renommé en waitgroupgo. Lancez-le une fois après avoir relevé la directive go :

example.bashbash
go fix -diff ./...   # preview
go fix ./...         # apply

Pour le catalogue complet de ce que go fix réécrit, consultez tous les modernizers de go fix 1.26.

go test exécute désormais par défaut la vérification vet stdversion. Elle signale les symboles de la bibliothèque standard plus récents que la version de Go en vigueur pour le fichier, telle que la fixent la directive de votre go.mod et vos build tags. Si votre module déclare go 1.25 et que quelqu'un utilise strings.CutLast, l'exécution des tests le détecte avant qu'un utilisateur en 1.25 ne tombe dessus. go test -json annote aussi les lignes "Action":"output" avec un champ optionnel "OutputType", qui vaut actuellement error, error-continue ou frame, ce qui est pratique si vous analysez la sortie des tests en CI.

go doc accepte désormais la syntaxe package@version : go doc rsc.io/[email protected] affiche la documentation d'une release précise sans avoir à la récupérer localement. Un nouveau flag -ex liste les exemples exécutables d'un package, et en nommer un affiche son code source.

go mod tidy impose une organisation en deux blocs pour les modules en go 1.27 ou ultérieur, en fusionnant les blocs require dispersés en un bloc direct et un bloc indirect. Les blocs de commentaires associés aux dépendances sont conservés, et un commentaire qui couvre un ensemble mixte est déplacé vers le bloc direct.

go tool trace -http=:6060 n'écoute plus que sur localhost, comme go tool pprof. Passez donc une adresse complète comme -http=0.0.0.0:6060 si vous devez y accéder depuis une autre machine. Les outils compile, link, asm, cgo, cover et pack acceptent aussi les fichiers de réponse (@file) dans un format compatible GCC, ce qui aide les systèmes de build qui dépassent les limites de longueur de ligne de commande.

Tests et net/http

httptest.NewTestServer crée un Server sur un faux réseau en mémoire, conçu pour être utilisé avec testing/synctest, sans aucun vrai port TCP en jeu. Son complément est synctest.Sleep, qui effectue time.Sleep et synctest.Wait en un seul appel : l'horloge simulée avance, puis les goroutines se stabilisent. Avec les deux, les tests HTTP ne dépendent plus du timing. Le cours Professional Go Testing couvre le modèle synctest sur lequel ils reposent.

example.gogo
srv := httptest.NewTestServer(t, handler) // in-memory, cleanup auto-registered

Côté net/http, le serveur HTTP/2 accepte désormais les signaux de priorité des clients définis par la RFC 9218 et sert en premier les streams de plus haute priorité. Définissez Server.DisableClientPriority = true pour retrouver l'ancien comportement en round-robin.

Le changement que vous avez le plus de chances de remarquer concerne HTTP/1 : fermer un Response.Body partiellement lu draine désormais le contenu restant, jusqu'à une limite prudente, afin que la connexion puisse être réutilisée. Pour la plupart des programmes, cela ne change rien ou apporte un léger gain de vitesse. Si vous fermez tôt pour interrompre un gros téléchargement, Transport.DisableKeepAlives = true désactive ce comportement.

Server.MaxHeaderValueCount limite le nombre de valeurs qu'un même en-tête peut porter, ce qui protège contre les requêtes qui inondent un en-tête. Transport et Server peuvent négocier TLS ALPN sur un net.Conn que vous fournissez vous-même, à condition qu'il implémente ConnectionState() tls.ConnectionState, ce qui permet d'utiliser HTTP/2 sur des connexions tunnelisées ou passant par un proxy. Enfin, net/url a gagné URL.Clone et Values.Clone pour les copies profondes. Le cours HTTP and Networking couvre les réglages de serveur et de transport sur lesquels ils reposent.

Verdict : à adopter dès maintenant. Si votre suite de tests HTTP est instable, httptest combiné à synctest justifie à lui seul la mise à jour.

Qu'est-ce qui casse lors du passage à Go 1.27 ?

Les release notes répartissent les changements cassants sur huit sections. Ce tableau les regroupe, classés selon la probabilité que vous les rencontriez.

Ce qui changeQui est touchéQue faire
compress/flate produit des octets différentsLes tests golden à l'octet près sur une sortie gzip, zlib, zip ou pngRégénérez les fixtures. La compression est correcte, simplement différente.
Noms plus simples pour les littéraux de fonction (closures)Les tests qui vérifient des noms de symboles, et le code qui compare l'égalité des pointeurs de code de fonctionsNe dépendez plus des noms des littéraux. La comparaison de pointeurs de fonctions était déjà documentée comme peu fiable.
GODEBUG asynctimerchan et gotypesalias supprimés définitivementTout ce qui les fige à l'ancienne valeur dans go.mod ou dans une ligne //go:debugLancez grep -r asynctimerchan . avant la mise à jour. Définir la valeur par défaut finale compile toujours, définir l'ancienne valeur échoue.
Cinq GODEBUG TLS/x509 supprimés : tlsunsafeekm, tlsrsakex, tls3des, tls10server, x509keypairleafLes services encore figés sur un comportement TLS historiqueMême règle que ci-dessus : la commande go accepte la valeur par défaut finale et rejette l'ancienne valeur, vous les repérez donc au moment du build.
macOS 13 Ventura est la version minimaleLes runners CI et les machines de développement sous un macOS plus ancienMettez à jour l'image du runner. Annoncé dans les notes de Go 1.26.
Prise en charge de bzr supprimée de la commande goLes modules hébergés sur des serveurs BazaarCréez un miroir de la dépendance, ou vendorez-la.
La nouvelle directive //go:linknamestd marque les linknames réservés à la std, le linker vérifie désormais l'accès par linkname aux symboles assembleur, et les descripteurs de type ont été déplacés dans une section .go.typeLes packages qui accèdent au runtime via un //go:linkname non autorisé, les vôtres ou ceux d'une dépendanceMettez à jour la dépendance. Ces erreurs surviennent au build, et bruyamment. Rien de tout cela ne figure dans les release notes, c'est donc le build qui vous l'apprend.
Response.Body.Close en HTTP/1 draine le contenu non luLe code qui ferme tôt pour interrompre un gros téléchargementDéfinissez Transport.DisableKeepAlives = true pour ces clients.
Le texte des messages d'erreur de json/v2 diffèreLes tests qui vérifient des chaînes d'erreur JSON exactesVérifiez le type d'erreur ou une sous-chaîne plutôt que le message complet.

La dernière ligne est la plus difficile à repérer. Le comportement est préservé, mais pas le texte, si bien qu'une suite de tests qui compare les erreurs d'unmarshaling sous forme de chaînes échoue sans cause évidente. Isolez le problème avec deux exécutions sur une branche où go.mod est déjà passé à go 1.27 :

example.bashbash
go test ./... > default.txt 2>&1
GOEXPERIMENT=nojsonv2 go test ./... > nojsonv2.txt 2>&1
diff default.txt nojsonv2.txt

Tout ce qui apparaît dans le diff vient du changement JSON. Tout ce qui échoue dans les deux exécutions relève d'une autre ligne du tableau. GOEXPERIMENT=nojsonv2 devrait être supprimé dans une future release : servez-vous-en pour diagnostiquer, puis corrigez les tests.

Questions fréquentes

Dois-je réécrire mon code JSON pour Go 1.27 ?

Non. Le package encoding/json est désormais implémenté au-dessus de v2, mais le comportement du marshaling et de l'unmarshaling est préservé et l'API v1 reste prise en charge. La seule différence observable est que le texte exact des messages d'erreur peut changer. Vous n'optez pour les valeurs par défaut plus strictes de v2 qu'en important explicitement encoding/json/v2.

Une méthode générique peut-elle satisfaire une interface ?

Non. Les release notes indiquent que les méthodes d'interface ne peuvent pas déclarer de paramètres de type, et que les méthodes d'interface ne peuvent pas être implémentées par des méthodes génériques. Le dispatch des interfaces se résout à l'exécution, le compilateur ne peut donc pas savoir quelles instanciations générer pour un appel résolu dynamiquement. Si votre API repose sur des interfaces, continuez à utiliser des fonctions génériques au niveau du package.

Faut-il abandonner github.com/google/uuid au profit du package de la bibliothèque standard ?

Pour du nouveau code, oui. Pour du code existant, vérifiez d'abord deux choses. Le UUID de la bibliothèque standard est un [16]byte doté de son propre ensemble de méthodes, donc tout code qui utilise les méthodes supplémentaires du type tiers doit être revu. Et toute dépendance qui expose le uuid.UUID tiers dans son API maintient de toute façon ce module dans votre graphe.

crypto/mldsa est-il prêt pour la production ?

C'est un package stable de la bibliothèque standard qui implémente FIPS 204, intégré à crypto/x509 et crypto/tls. La contrainte pratique est la taille des signatures, pas la maturité : les signatures ML-DSA-65 font 3 309 octets, contre 64 pour Ed25519. Utilisez-le là où la signature doit survivre à la cryptographie actuelle, et laissez-le de côté pour les jetons à courte durée de vie émis en grand volume.

Comment repérer au plus vite ce que Go 1.27 casse dans mon code ?

Passez go.mod à go 1.27 sur une branche, puis cherchez avec grep asynctimerchan, gotypesalias et les cinq réglages TLS supprimés, car ceux-ci font échouer le build directement. Lancez ensuite votre suite deux fois, une fois normalement et une fois avec GOEXPERIMENT=nojsonv2, et comparez les échecs avec diff. Cela distingue les écarts de texte des erreurs JSON des échecs liés aux fichiers golden et aux noms de symboles.

Sources

Références principales citées dans cet article (dernière vérification le 23 août 2026) :

Pour aller plus loin

Les méthodes génériques sont plus faciles à bien utiliser une fois que les contraintes et l'inférence vous sont devenues naturelles, et ce sont justement les parties que l'on a tendance à survoler en découvrant les génériques. Le cours Go Generics Masterclass aborde les contraintes, les ensembles de types et l'inférence sous forme d'exercices dans le navigateur qui tournent sur la toolchain actuelle, pour que vous puissiez essayer la syntaxe des méthodes de Go 1.27 dans du vrai code.

Vous débutez en Go ? Commencez par le parcours Go Fundamentals et revenez ensuite pour les release notes. Et si vous avez fait l'impasse sur la release de l'an dernier, Nouveautés de Go 1.26 couvre errors.AsType, le garbage collector Green Tea et new(expr).

Écrivez du Go comme un ingénieur senior

Des leçons interactives dans votre navigateur. Les premières sont gratuites.

Essayer une leçon gratuiteOu créer un compte gratuit