Retour au blog

Nouveautés de Go 1.26 : le guide complet

Un guide complet des nouveautés de Go 1.26 : gestion des erreurs améliorée avec errors.AsType, API de cryptographie simplifiées, détection des fuites de goroutines, gains de performances du GC Green Tea, et plus encore. Avec des exemples de code exécutables.

Nouveautés de Go 1.26 : le guide complet

L'essentiel de Go 1.26 se retrouve dans le code que vous écrivez tous les jours. Moins de code répétitif autour des pointeurs et des types d'erreur, une nouvelle façon de repérer les fuites de goroutines, et un runtime plus rapide dont vous profitez simplement en recompilant.

Table des matières


Initialiser un pointeur plus simplement avec new(expr)

Si vous avez déjà utilisé une API Go qui signale les champs optionnels par des pointeurs, vous avez écrit ceci :

example.gogo
port := 8080
config := &Config{Port: &port}  // Can't use &8080 directly!

Go ne vous laisse pas prendre l'adresse d'une valeur littérale, il faut donc d'abord passer par une variable temporaire. Certains projets ajoutent des helpers comme intPtr(n int) *int, mais cela ne fait que déplacer le problème.

Go 1.26 règle la question en étendant la fonction intégrée new(). Elle n'acceptait jusqu'ici que des types : new(int) renvoyait un *int pointant vers zéro. Elle accepte désormais n'importe quelle expression :

example.gogo
package main

import "fmt"

type ServerConfig struct {
    Host    string
    Port    *int
    Enabled *bool
}

func main() {
    config := ServerConfig{
        Host:    "localhost",
        Port:    new(8080),
        Enabled: new(true),
    }

    fmt.Printf("Port: %d, Enabled: %t\n", *config.Port, *config.Enabled)
}

Quand vous écrivez new(8080), Go évalue l'expression, alloue la mémoire pour son type (ici un int), y stocke la valeur et renvoie un pointeur. Toute expression convient : new(x + y) pour une valeur calculée, new(time.Now()) pour le résultat d'une fonction, ou new("default") pour une chaîne.

Vous vous en servirez surtout avec les API JSON, les messages Protobuf et toute autre struct où un champ pointeur distingue « optionnel, avec une valeur » de « non fourni ».

Vérifier les erreurs de façon typée avec errors.AsType

Pour extraire un type d'erreur précis d'une erreur enveloppée, vous utilisiez jusqu'ici errors.As() :

example.gogo
var appErr *AppError
if errors.As(err, &appErr) {
    fmt.Printf("Code: %d\n", appErr.Code)
}

Cela fonctionne, mais c'est maladroit. La variable déborde dans la portée englobante alors que vous ne l'utilisez que dans le bloc if. Vous passez &appErr alors que appErr est déjà un pointeur, ce qui en piège plus d'un. Et errors.As repose sur la réflexion en interne.

Go 1.26 ajoute errors.AsType[T](), une fonction générique qui fait le même travail avec moins de cérémonie :

example.gogo
package main

import (
    "errors"
    "fmt"
)

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("app error %d: %s", e.Code, e.Message)
}

func main() {
    err := &AppError{Code: 404, Message: "not found"}

    if appErr, ok := errors.AsType[*AppError](err); ok {
        fmt.Printf("Code: %d, Message: %s\n", appErr.Code, appErr.Message)
    }

    // Works with wrapped errors too
    wrappedErr := fmt.Errorf("operation failed: %w", err)
    if appErr, ok := errors.AsType[*AppError](wrappedErr); ok {
        fmt.Printf("Unwrapped code: %d\n", appErr.Code)
    }
}

Le paramètre de type [*AppError] indique au compilateur exactement ce que vous cherchez, et le résultat reste limité au bloc if. Plus de pointeur vers un pointeur. Comme le type est connu à la compilation, il n'y a pas non plus de réflexion, ce qui accélère les chemins critiques de gestion des erreurs.

Contraintes génériques autoréférentielles

Go 1.26 prend en charge, de façon limitée, les contraintes de type récursives. Certains patterns deviennent ainsi exprimables pour la première fois, comme un type qui se compare à d'autres valeurs du même type :

example.gogo
package main

import "fmt"

type Comparable[T any] interface {
    CompareTo(other T) int
}

type Integer int

func (i Integer) CompareTo(other Integer) int {
    if i < other {
        return -1
    } else if i > other {
        return 1
    }
    return 0
}

func Max[T Comparable[T]](a, b T) T {
    if a.CompareTo(b) > 0 {
        return a
    }
    return b
}

func main() {
    result := Max(Integer(5), Integer(3))
    fmt.Printf("Max: %d\n", result)
}

La contrainte Comparable[T] fait référence à son propre paramètre de type, et Max[T Comparable[T]] exige que T puisse se comparer à lui-même. Avant Go 1.26, ce genre de contrainte générique autoréférentielle ne compilait pas. Vous pouvez désormais construire des API fluides avec chaînage de méthodes, des arbres dont les nœuds référencent leur propre type, et des builders typés qui renvoient le bon type à chaque étape.

Des API de cryptographie plus simples

La plupart des développeurs Go ont déjà écrit cette ligne :

example.gogo
key, err := rsa.GenerateKey(rand.Reader, 2048)

L'argument rand.Reader est un vestige historique. Les premières versions de Go autorisaient des sources aléatoires personnalisées pour les tests. En pratique, vous devez toujours passer crypto/rand.Reader, et passer autre chose, comme math/rand, ouvre une faille de sécurité.

Go 1.26 fait du choix sûr le comportement par défaut. Passez nil et la fonction utilise crypto/rand.Reader en interne :

example.gogo
package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
)

func main() {
    key := make([]byte, 32)
    rand.Read(key)

    fmt.Printf("Generated key: %s\n", hex.EncodeToString(key))
}

Cela vaut pour la génération de clés dans tous les packages crypto : rsa.GenerateKey(nil, bits), ecdsa.GenerateKey(curve, nil) et ecdh.GenerateKey(curve, nil). La seule raison qui reste de passer votre propre reader est un test qui a besoin d'une sortie déterministe.

HPKE pour un chiffrement moderne

Le nouveau package crypto/hpke implémente la RFC 9180, un standard moderne de chiffrement hybride à clé publique. RSA seul est lent et ne peut chiffrer que de petits messages. La solution habituelle consiste à chiffrer une clé symétrique aléatoire avec RSA, puis à utiliser cette clé pour les données elles-mêmes. Cela fonctionne, mais l'implémentation demande beaucoup de soin.

HPKE standardise ce pattern avec des algorithmes modernes. Imaginez un coffret. L'expéditeur fabrique un cadenas et une clé à usage unique, enferme le message dans le coffret, puis l'envoie accompagné d'instructions que seule la clé privée du destinataire permet d'utiliser pour recréer la clé à usage unique.

example.gogo
suite := hpke.NewSuite(
    hpke.DHKEM_X25519,
    hpke.KDF_HKDF_SHA256,
    hpke.AEAD_ChaCha20Poly1305,
)

publicKey, privateKey, _ := suite.GenerateKeyPair(nil)

sender, _ := suite.NewSender(publicKey, nil)
ciphertext, _ := sender.Seal(plaintext, nil)
encapsulated := sender.EncapsulatedKey()

recipient, _ := suite.NewRecipient(privateKey, encapsulated)
decrypted, _ := recipient.Open(ciphertext, nil)

HPKE utilise des clés de 256 bits au lieu des 2 048 à 4 096 bits de RSA, gère des messages de n'importe quelle taille et se montre bien plus rapide.

Détecter les fuites de goroutines

Les fuites de goroutines passent facilement inaperçues. Une fuite mémoire finit par faire planter votre programme. Une fuite de goroutines, elle, le ralentit peu à peu, à mesure que des milliers de goroutines bloquées s'accumulent et retiennent mémoire et CPU sans rien faire d'utile.

Voici une façon courante d'en provoquer une :

example.gogo
func handleRequest(ctx context.Context) error {
    results := make(chan Result)

    go func() {
        result := expensiveOperation()
        results <- result  // Blocks forever if context cancels
    }()

    select {
    case <-ctx.Done():
        return ctx.Err()  // Goroutine still blocked on send
    case r := <-results:
        return processResult(r)
    }
}

Si le contexte est annulé avant le retour de expensiveOperation(), la goroutine reste bloquée indéfiniment en essayant d'envoyer sur un channel non bufferisé que personne ne lira jamais.

Go 1.26 ajoute un profil goroutineleak et une option go test -goroutineleak qui détectent ces cas automatiquement. Vous pouvez aussi mettre une fuite en évidence en comptant les goroutines avant et après :

example.gogo
package main

import (
    "fmt"
    "runtime"
    "time"
)

func createLeak() {
    ch := make(chan int)
    go func() {
        <-ch  // Blocks forever - no sender
    }()
}

func main() {
    before := runtime.NumGoroutine()
    createLeak()
    time.Sleep(50 * time.Millisecond)
    after := runtime.NumGoroutine()

    fmt.Printf("Before: %d, After: %d\n", before, after)
    if after > before {
        fmt.Println("Goroutine leak detected!")
    }
}

La correction tient en trois éléments : un channel bufferisé pour que l'envoi ne bloque pas, un select dans la goroutine pour qu'elle puisse se terminer quand le contexte est annulé, et une vérification du contexte côté appelant :

example.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func expensiveWork() string {
    time.Sleep(50 * time.Millisecond)
    return "completed"
}

func safeHandler(ctx context.Context) (string, error) {
    ch := make(chan string, 1)

    go func() {
        result := expensiveWork()
        select {
        case ch <- result:
        case <-ctx.Done():
            return
        }
    }()

    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case result := <-ch:
        return result, nil
    }
}

func main() {
    ctx1 := context.Background()
    result1, err1 := safeHandler(ctx1)
    fmt.Printf("Normal: %s, err: %v\n", result1, err1)

    ctx2, cancel := context.WithCancel(context.Background())
    cancel()
    result2, err2 := safeHandler(ctx2)
    fmt.Printf("Canceled: %s, err: %v\n", result2, err2)
}

Avec le channel bufferisé, l'envoi ne peut pas bloquer, même si personne ne reçoit. Le select offre à la goroutine une porte de sortie quand le contexte est annulé. Que le travail aboutisse ou soit annulé, la goroutine se termine.

Go 1.26 ajoute aussi des métriques sur les goroutines dans runtime/metrics, pour que vos tableaux de bord de production suivent le nombre total de goroutines, ainsi que le nombre de goroutines bloquées et prêtes à s'exécuter.

Améliorations de la bibliothèque standard

Buffer.Peek

Lire depuis un bytes.Buffer consomme les données. Si vous vouliez d'abord regarder ce qui arrive, par exemple pour vérifier le type d'un message avant de choisir un parser, ou pour valider un format avant de le lire, vous deviez copier le buffer.

Go 1.26 ajoute Peek() :

example.gogo
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("Hello, World!")

    peeked := buf.Bytes()[:5]
    fmt.Printf("Peeked: %s\n", peeked)

    fmt.Printf("Full content: %s\n", buf.String())
    fmt.Printf("Length: %d\n", buf.Len())
}

Comparer des préfixes IP

Trier des préfixes IP obligeait auparavant à comparer les octets à la main. netip.Prefix dispose désormais d'une méthode Compare() qui se branche directement sur slices.SortFunc :

example.gogo
package main

import (
    "fmt"
    "net/netip"
    "slices"
)

func main() {
    prefixes := []netip.Prefix{
        netip.MustParsePrefix("192.168.0.0/16"),
        netip.MustParsePrefix("10.0.0.0/8"),
        netip.MustParsePrefix("172.16.0.0/12"),
    }

    slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
        return a.Compare(b)
    })

    for _, p := range prefixes {
        fmt.Println(p)
    }
}

Connaître le signal à l'origine de l'annulation

Si vous utilisez signal.NotifyContext() pour un arrêt propre, vous pouvez maintenant savoir quel signal a provoqué l'annulation :

example.gogo
ctx, stop := signal.NotifyContext(
    context.Background(),
    os.Interrupt,
    syscall.SIGTERM,
)
defer stop()

<-ctx.Done()

if cause := context.Cause(ctx); cause != nil {
    switch cause {
    case os.Interrupt:
        log.Println("User interrupted (Ctrl+C)")
    case syscall.SIGTERM:
        log.Println("Graceful shutdown requested")
    }
}

Tests et journalisation

slog.MultiHandler

Les services en production écrivent souvent leurs logs à plusieurs endroits : du JSON dans un fichier pour l'agrégation des logs, du texte sur stdout pour le débogage, les erreurs vers un service de monitoring. slog.MultiHandler se charge de cette diffusion pour vous, sans que vous ayez à l'écrire vous-même :

example.gogo
package main

import (
    "log/slog"
    "os"
)

func main() {
    jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
    logger := slog.New(jsonHandler)
    logger.Info("Server started", "port", 8080)
}

Chaque handler filtre selon son propre niveau. Le fichier peut tout recevoir dès le niveau debug, tandis que la console ne reçoit que les avertissements et les erreurs.

Répertoires d'artefacts de test

Les tests d'intégration écrivent souvent des fichiers de sortie : captures d'écran, dumps SQL, rapports générés. Ces fichiers finissaient dispersés dans /tmp ou un autre répertoire, et rien ne les nettoyait.

t.ArtifactDir() leur donne un emplacement standard. Il est nettoyé quand le test réussit et conservé quand il échoue :

example.gogo
func TestGenerateReport(t *testing.T) {
    artifactDir := t.ArtifactDir()
    reportPath := filepath.Join(artifactDir, "report.txt")
    
    err := os.WriteFile(reportPath, []byte("Test Report\n"), 0644)
    if err != nil {
        t.Fatalf("Failed to write: %v", err)
    }
}

C'est la conservation des fichiers en cas d'échec qui rend la fonctionnalité utile. Vous pouvez ouvrir la sortie d'un test en échec sans avoir à la chercher.

Améliorations de httptest

Tester du code HTTPS qui communique avec des hôtes externes impliquait de désactiver la vérification des certificats (une mauvaise idée, même dans les tests) ou de mettre en place une configuration de certificats laborieuse. Avec Go 1.26, httptest.Server.Client() redirige les requêtes destinées à example.com vers votre serveur de test et gère TLS pour vous :

example.gogo
func TestExternalAPIClient(t *testing.T) {
    server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer server.Close()

    client := server.Client()
    resp, err := client.Get("https://example.com/api/status")
    // Request goes to test server, not real example.com
}

Performances : le GC Green Tea

Aucune de ces améliorations ne demande de modifier votre code. Recompilez avec Go 1.26 et votre programme s'exécute plus vite.

Le nouveau garbage collector Green Tea réduit la latence P99 de 30 à 40 % et le surcoût mémoire de 10 à 15 %, et augmente le débit de 5 à 10 %. Les programmes qui allouent beaucoup d'objets à courte durée de vie en profitent le plus. Ces gains viennent d'une meilleure planification du travail du GC et d'un surcoût réduit pour le suivi des allocations.

Les appels système et les appels cgo sont environ 30 % plus rapides, car le runtime a abandonné l'état de processeur _Psyscall, qui alourdissait chaque syscall. L'allocation de petits objets (1 à 512 octets) est elle aussi environ 30 % plus rapide, grâce à des routines d'allocation spécialisées qui utilisent des tables de saut au lieu de chemins génériques.

Certaines fonctions de la bibliothèque standard ont aussi gagné en vitesse. fmt.Errorf est environ 30 % plus rapide et io.ReadAll 28 % plus rapide. Lors de tests en production, un serveur d'API HTTP traitant 10 000 requêtes par seconde a vu sa latence P99 passer de 45 ms à 32 ms.

Nouveautés du runtime

Connexions réseau tenant compte du contexte

Le package net applique désormais les échéances du contexte à la résolution DNS en plus de la connexion elle-même :

example.gogo
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", "example.com:443")
// Timeout now covers DNS + connect time combined

Protéger les données sensibles

Le package expérimental runtime/secret aide à tenir les données sensibles à l'écart des dumps mémoire :

example.gogo
secretKey := secret.New([]byte("sk_live_secret"))
defer secretKey.Destroy()

apiKey := secretKey.Bytes()
makeAuthenticatedRequest(apiKey)

Destroy() efface la mémoire, ce qui réduit la fenêtre pendant laquelle un secret peut apparaître dans un core dump ou un fichier d'échange.

Opérations SIMD

Le nouveau package simd/archsimd donne accès aux instructions vectorielles matérielles sur amd64 :

example.gogo
func vectorAdd(a, b [4]float32) [4]float32 {
    va := archsimd.Float32x4FromArray(a)
    vb := archsimd.Float32x4FromArray(b)
    return va.Add(vb).ToArray()
}

Le SIMD traite plusieurs valeurs par instruction, ce qui multiplie le débit par 4 à 16 pour les calculs numériques en traitement d'image, dans les codecs audio et vidéo et en calcul scientifique. Il dépend de la plateforme : prévoyez donc une version scalaire de repli si votre code doit tourner sur d'autres architectures.

Migrer votre code

Go 1.26 améliore go fix pour qu'il automatise les migrations courantes :

example.bashbash
go fix -diff ./...   # Preview changes
go fix ./...         # Apply all fixes

Il réécrit rsa.GenerateKey(rand.Reader, 2048) en rsa.GenerateKey(nil, 2048) et convertit les patterns errors.As en errors.AsType lorsque c'est possible. Relisez le diff, lancez vos tests et faites un commit.

La liste complète des réécritures se trouve dans tous les modernizers de go fix 1.26.

Ce que cela change pour votre code

Les changements que vous rencontrerez en premier sont new(expr) et errors.AsType, et go fix se charge d'une partie de cette migration pour vous. Lancez go test -goroutineleak sur votre code concurrent pour trouver les fuites avant qu'elles n'atteignent la production. Les générateurs de clés crypto choisissent désormais la source aléatoire sûre quand vous passez nil. Les gains de vitesse du GC et du runtime ne vous demandent rien de plus qu'une recompilation.

É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