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)
- Vérifier les erreurs de façon typée avec errors.AsType
- Contraintes génériques autoréférentielles
- Des API de cryptographie plus simples
- Détecter les fuites de goroutines
- Améliorations de la bibliothèque standard
- Tests et journalisation
- Performances : le GC Green Tea
- Nouveautés du runtime
- Migrer votre code
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.gogoport := 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.gogopackage 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.gogovar 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.gogopackage 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.gogopackage 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.gogokey, 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.gogopackage 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.gogosuite := 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.gogofunc 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogoctx, 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.gogopackage 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.gogofunc 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.gogofunc 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.gogoctx, 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.gogosecretKey := 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.gogofunc 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.bashbashgo 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.
