Go 1.26 a réécrit go fix sur le même framework que go vet. La nouvelle commande regroupe tous les modernizers écrits par l'équipe Go et les auteurs de x/tools. Chacun est un petit analyseur qui reconnaît un ancien motif et le réécrit en Go actuel.
Une seule commande les exécute tous :
example.bashbashgo fix ./...
Lancez-la sur une branche propre, relisez le diff, exécutez vos tests, puis faites un commit. Pour avoir un aperçu sans écrire de fichiers, utilisez go fix -diff ./....
Vous trouverez ci-dessous tous les modernizers livrés avec la 1.26, regroupés par domaine. Chaque entrée indique la version de Go qui a introduit l'API cible de la réécriture, avec une paire avant/après. Cet article se parcourt en diagonale ou se consulte avec ctrl-F autant qu'il se lit.
Table des matières
- Langage et fonctions natives
- Slices, maps et itération
- Chaînes et formatage
- Réseau, erreurs et réflexion
- Concurrence et tests
- Exécuter go fix en toute sécurité
Langage et fonctions natives
Ce groupe compte sept modernizers. La plupart concernent des fonctionnalités du langage (any, la nouvelle forme range n, le changement de variable de boucle de la 1.22, l'extension new(expr)), auxquelles s'ajoutent quelques nettoyages valables quelle que soit la version.
any (Go 1.18) : remplacer interface{} par any
any n'est qu'un alias, mais il se lit mieux dans les signatures et les paramètres de type.
Avant :
example.gogofunc log(args ...interface{}) { fmt.Println(args...) } var cache map[string]interface{}
Après :
example.gogofunc log(args ...any) { fmt.Println(args...) } var cache map[string]any
minmax (Go 1.21) : remplacer les bornages if/else par min et max
Les fonctions natives min et max sont variadiques et acceptent n'importe quel type ordonné.
Avant :
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
Après :
example.gogox := max(0, min(100, f()))
rangeint (Go 1.22) : les boucles for sur entiers en for i := range n
Une simple boucle de comptage, sans slice ni tableau derrière, peut itérer directement sur l'entier.
Avant :
example.gogofor i := 0; i < n; i++ { fmt.Println(i) }
Après :
example.gogofor i := range n { fmt.Println(i) }
forvar (Go 1.22) : supprimer l'astuce de capture x := x
Avant la 1.22, la variable de boucle était partagée entre les itérations. Les closures et les goroutines qui la capturaient avaient donc besoin de x := x dans le corps de la boucle. Depuis la 1.22, chaque itération a sa propre variable, ce qui rend ce masquage inutile.
Avant :
example.gogofor _, x := range items { x := x // pre-1.22 capture trick go func() { use(x) }() }
Après :
example.gogofor _, x := range items { go func() { use(x) }() }
L'analyseur ne se déclenche que si go.mod déclare go 1.22 ou plus, car les modules plus anciens conservent la sémantique de variable partagée.
newexpr (Go 1.26) : un pointeur vers une valeur avec new(expr)
La 1.26 étend la fonction native new pour qu'elle accepte une expression, et pas seulement un type. new(s) renvoie un *string qui pointe vers une copie de s. Les habituels helpers strPtr/intPtr/boolPtr deviennent donc inutiles.
Avant :
example.gogofunc strPtr(s string) *string { p := s return &p } cfg.Name = strPtr("LevelUpGo")
Après :
example.gogocfg.Name = new("LevelUpGo")
Si vous avez des helpers que vous ne pouvez pas encore supprimer (autres appelants, API exportée), consultez la section inline ci-dessous pour la directive qui permet de migrer progressivement.
inline (directive) : intégrer les fonctions et constantes marquées //go:fix inline
Ajoutez la directive à un wrapper, et go fix réécrit chaque site d'appel pour utiliser l'expression interne. Je la trouve pratique pour les renommages et pour retirer des wrappers minces. Vous pouvez aussi vous en servir pour créer vos propres modernizers sur une base de code privée.
Avant :
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Square(5) b := Square(10)
Après :
example.gogo//go:fix inline func Square(x int) int { return Pow(x, 2) } a := Pow(5, 2) b := Pow(10, 2)
La même directive fonctionne sur les constantes, ce qui la rend utile pour migrer entre des valeurs d'énumération renommées.
plusbuild (Go 1.17) : remplacer // +build par //go:build
L'ancienne syntaxe des build tags est dépréciée depuis la 1.17. La nouvelle directive utilise des expressions booléennes Go classiques.
Avant :
example.gogo// +build linux,amd64 package sys
Après :
example.gogo//go:build linux && amd64 package sys
Slices, maps et itération
Cinq des sept modernizers de ce groupe remplacent des boucles écrites à la main par les packages slices et maps. Les deux autres traitent reflect.TypeFor et les tags JSON omitzero.
slicessort (Go 1.21) : sort.Slice devient slices.Sort
Dans le cas courant du tri croissant d'un slice ordonné, slices.Sort est générique et se passe complètement de la closure de comparaison.
Avant :
example.gogosort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
Après :
example.gogoslices.Sort(s)
slicescontains (Go 1.21) : les boucles de recherche manuelles deviennent slices.Contains
Avant :
example.gogofunc has(s []int, x int) bool { for _, v := range s { if v == x { return true } } return false }
Après :
example.gogofunc has(s []int, x int) bool { return slices.Contains(s, x) }
slicesbackward (Go 1.23) : itération inverse avec slices.Backward
slices.Backward renvoie un itérateur qui produit les paires index/valeur de la fin du slice jusqu'à son début.
Avant :
example.gogofor i := len(s) - 1; i >= 0; i-- { use(i, s[i]) }
Après :
example.gogofor i, v := range slices.Backward(s) { use(i, v) }
mapsloop (Go 1.21) : les boucles de copie, de clonage et d'insertion deviennent des appels maps
Le package maps fournit une fonction pour chacune des boucles courantes sur les maps. Le nom de l'appel indique donc au lecteur ce que faisait la boucle.
Avant :
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) for k, v := range src { dst[k] = v } return dst }
Après :
example.gogofunc copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) maps.Copy(dst, src) return dst }
Le même modernizer couvre maps.Clone, maps.Equal et le motif d'insertion à partir de paires.
stditerators (Go 1.23) : utiliser les itérateurs de la stdlib dans les boucles range
maps.Keys, maps.Values, slices.All, slices.Values et consorts renvoient des itérateurs que vous pouvez parcourir directement avec range. Inutile donc de construire d'abord un slice intermédiaire.
Avant :
example.gogokeys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } for _, k := range keys { use(k) }
Après :
example.gogofor k := range maps.Keys(m) { use(k) }
reflecttypefor (Go 1.22) : abandonner l'astuce reflect du pointeur nil
reflect.TypeFor[T]() est la manière typée et générique d'obtenir un reflect.Type pour un type statique. L'ancien contournement TypeOf((*T)(nil)).Elem() date d'avant les generics.
Avant :
example.gogot := reflect.TypeOf((*MyType)(nil)).Elem()
Après :
example.gogot := reflect.TypeFor[MyType]()
omitzero (Go 1.24) : remplacer le fragile omitempty par omitzero
omitempty se fonde sur la valeur zéro au sens de Go. Un time.Time{} n'est donc pas « vide » (c'est un struct avec des champs) et n'est jamais omis. omitzero appelle la méthode IsZero de la valeur quand elle existe, si bien que les valeurs zéro de time.Time disparaissent de la sortie comme on s'y attend.
L'analyseur ne modifie que les champs pour lesquels omitzero est sûr. Les chaînes, les slices et les pointeurs restent en omitempty, car leur comportement changerait.
Avant :
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitempty"` }
Après :
example.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitzero"` }
Chaînes et formatage
Ces cinq modernizers remplacent d'anciens motifs strings et fmt par des API plus récentes. Certains économisent une allocation, d'autres sont simplement plus lisibles.
fmtappendf (Go 1.19) : []byte(fmt.Sprintf(...)) devient fmt.Appendf
fmt.Appendf écrit la sortie formatée directement dans un []byte que vous fournissez (ou nil pour une nouvelle allocation), sans passer par la chaîne intermédiaire.
Avant :
example.gogob := []byte(fmt.Sprintf("%s=%d", key, val))
Après :
example.gogob := fmt.Appendf(nil, "%s=%d", key, val)
stringscut (Go 1.18) : strings.Index suivi d'un découpage devient strings.Cut
strings.Cut renvoie le préfixe, le suffixe et un booléen found en un seul appel. Il gère aussi explicitement le cas où le séparateur est introuvable.
Avant :
example.gogoi := strings.Index(s, "=") if i >= 0 { key, val := s[:i], s[i+1:] use(key, val) }
Après :
example.gogoif key, val, ok := strings.Cut(s, "="); ok { use(key, val) }
stringscutprefix (Go 1.20) : HasPrefix suivi de TrimPrefix devient CutPrefix
CutPrefix renvoie la chaîne tronquée et un booléen found. Vous ne vérifiez donc le préfixe qu'une seule fois.
Avant :
example.gogoif strings.HasPrefix(s, "go-") { rest := strings.TrimPrefix(s, "go-") handle(rest) }
Après :
example.gogoif rest, ok := strings.CutPrefix(s, "go-"); ok { handle(rest) }
Le même modernizer traite CutSuffix.
stringsseq (Go 1.24) : Split et Fields dans les boucles range deviennent SplitSeq et FieldsSeq
SplitSeq et FieldsSeq renvoient des itérateurs au lieu d'allouer un slice. Si vous n'avez besoin de parcourir les morceaux qu'une fois, la forme itérateur évite complètement l'allocation.
Avant :
example.gogofor _, line := range strings.Split(s, "\n") { handle(line) }
Après :
example.gogofor line := range strings.SplitSeq(s, "\n") { handle(line) }
L'analyseur ne réécrit que les cas où le slice est consommé une fois puis abandonné. Si vous stockez le slice ou y accédez par index, la forme Split d'origine est conservée.
stringsbuilder (Go 1.10) : la concaténation quadratique en boucle devient strings.Builder
s += p dans une boucle réalloue et recopie à chaque itération, ce qui donne un travail en O(n²). strings.Builder grandit comme un slice.
Avant :
example.gogos := "" for _, p := range parts { s += p } return s
Après :
example.gogovar sb strings.Builder for _, p := range parts { sb.WriteString(p) } return sb.String()
L'analyseur se montre prudent ici. Il ne se déclenche que sur le motif évident d'accumulateur, pas sur chaque += dans une boucle. Si votre boucle concatène deux ou trois petites constantes, la forme simple convient très bien.
Réseau, erreurs et réflexion
Ce groupe en compte trois : un vieux piège de net, le nouveau errors.AsType générique et les helpers unsafe plus sûrs.
hostport (Go 1.0) : Sprintf pour les adresses devient net.JoinHostPort
fmt.Sprintf("%s:%d", host, port) produit une adresse invalide quand host est un littéral IPv6, car les adresses IPv6 contiennent elles-mêmes des deux-points. net.JoinHostPort entoure les hôtes IPv6 de crochets et constitue la manière canonique de construire une chaîne host:port.
Avant :
example.gogoaddr := fmt.Sprintf("%s:%d", host, port) net.Dial("tcp", addr)
Après :
example.gogoaddr := net.JoinHostPort(host, strconv.Itoa(port)) net.Dial("tcp", addr)
errorsastype (Go 1.26) : errors.As devient le générique errors.AsType
La 1.26 ajoute errors.AsType[T], qui renvoie l'erreur typée et un booléen, au lieu de vous obliger à déclarer une variable cible et à passer son adresse.
Avant :
example.gogovar myerr *MyErr if errors.As(err, &myerr) { handle(myerr) }
Après :
example.gogoif myerr, ok := errors.AsType[*MyErr](err); ok { handle(myerr) }
unsafefuncs (Go 1.17) : l'arithmétique unsafe devient unsafe.Add et unsafe.Slice
La 1.17 a ajouté unsafe.Add et unsafe.Slice pour faire de l'arithmétique de pointeurs et construire des slices sur une zone mémoire arbitraire sans écrire à la main toute la gymnastique avec uintptr.
Avant (base est un unsafe.Pointer) :
example.gogop := unsafe.Pointer(uintptr(base) + uintptr(off))
Après :
example.gogop := unsafe.Add(base, off)
Le même modernizer réécrit en unsafe.Slice les motifs manuels qui combinent unsafe.Pointer et reflect.SliceHeader.
Concurrence et tests
Les quatre derniers nettoient le code qui utilise sync, atomic et le package testing.
waitgroupgo (Go 1.25) : Add(1) suivi de defer Done() devient wg.Go
La 1.25 a ajouté WaitGroup.Go, qui réunit le Add, le lancement de la goroutine et le defer Done en un seul appel.
Avant :
example.gogovar wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("go!") }() wg.Wait()
Après :
example.gogovar wg sync.WaitGroup wg.Go(func() { fmt.Println("go!") }) wg.Wait()
atomictypes (Go 1.19) : les atomiques sous forme de fonctions deviennent des méthodes
atomic.Int64, atomic.Uint64, atomic.Pointer[T] et consorts portent leur propre valeur. Vous arrêtez donc de faire circuler des pointeurs et de confondre la taille de l'opération avec le type de la variable.
Avant :
example.gogovar count int64 atomic.AddInt64(&count, 1) n := atomic.LoadInt64(&count)
Après :
example.gogovar count atomic.Int64 count.Add(1) n := count.Load()
testingcontext (Go 1.24) : le WithCancel manuel dans les tests devient t.Context()
t.Context() renvoie un context annulé automatiquement à la fin du test. Vous n'avez plus à penser au defer cancel(), et le context se propage aux sous-tests.
Avant :
example.gogofunc TestThing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() work(ctx) }
Après :
example.gogofunc TestThing(t *testing.T) { ctx := t.Context() work(ctx) }
bloop (Go 1.24) : for i := 0; i < b.N; i++ devient for b.Loop()
b.Loop() est la nouvelle boucle de benchmark. Elle gère aussi automatiquement la réinitialisation et l'arrêt du timer, si bien que les appels manuels à b.ResetTimer() et b.StopTimer() autour de la préparation disparaissent dans la plupart des benchmarks.
Avant :
example.gogofunc BenchmarkThing(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { work() } }
Après :
example.gogofunc BenchmarkThing(b *testing.B) { for b.Loop() { work() } }
Exécuter go fix en toute sécurité
go fix réécrit de vrais fichiers source. Traitez-le comme n'importe quel autre refactoring automatisé.
Un ordre d'opérations sûr :
- Partez d'une branche propre, sans autre modification en cours.
- Lancez d'abord
go fix -diff ./.... Ce flag affiche un diff unifié sur la sortie standard au lieu d'écrire les fichiers. Parcourez-le. - Lancez
go fix ./...pour appliquer les changements. - Lancez
git diffsur les modifications réelles. Repérez toute réécriture qui vous surprend, en particulier dans du code volontairement écrit dans un style plus ancien. - Exécutez vos tests, votre linter et
go build ./.... - Faites le commit.
Une seule passe ne suffit souvent pas
Appliquer un correctif peut faire apparaître une occasion d'en appliquer un autre, et les analyseurs ne suivent pas cette cascade au cours d'une même passe.
Le bornage de la section minmax l'illustre. Partons de :
example.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
La première passe réécrit la borne inférieure en max :
example.gogox := max(0, f()) if x > 100 { x = 100 }
La deuxième passe voit le motif if/affectation restant et termine le travail :
example.gogox := min(100, max(0, f()))
Il faut deux passes parce que chaque modernizer est un analyseur distinct qui examine votre code une fois par exécution de go fix. Il parcourt l'arbre syntaxique, trouve les motifs qu'il connaît et émet les correctifs correspondants.
Dans l'exemple du bornage, la première passe réécrit le bloc < 0. Après cette modification, le bloc > 100 suit un appel max(...) au lieu d'un simple x := f(), ce qui constitue une nouvelle occasion pour minmax. Mais minmax a déjà fait son unique passage, il ne verra donc la nouvelle forme qu'à la prochaine exécution de go fix.
Le même phénomène se produit entre analyseurs. Une réécriture de mapsloop peut faire apparaître une occasion pour stditerators, car l'analyseur d'itérateurs cherchait une forme range maps.Keys(m) qui n'existait pas avant la réécriture de la boucle.
La solution consiste à relancer go fix ./... jusqu'à ce que le diff soit vide. Deux passes suffisent pour la plupart des bases de code, et une troisième est rare.
Autres remarques
- Les modernizers liés à une version (
forvar,rangeint,newexpr,errorsastype) ne se déclenchent que sigo.moddéclare la bonne version minimale. Si vous ne voyez pas la réécriture attendue, vérifiez votre directivego. - Pour les migrations propres à votre base de code, utilisez la directive
//go:fix inline. Marquez un wrapper ou une constante, lancezgo fix, et chaque site d'appel est mis à jour sur place. - Si une réécriture change le comportement d'une manière que l'analyseur n'a pas détectée, c'est un bug de l'analyseur qui mérite d'être signalé. L'équipe Go traite ces rapports rapidement.
Pour aller plus loin
Pour une couverture complète de la version Go 1.26 (nouvelles API crypto, Green Tea GC, détection des fuites de goroutines, etc.), consultez le guide des nouveautés de Go 1.26.
En amont, la documentation de x/tools/go/analysis/passes/modernize détaille chaque analyseur, et l'article du blog Go sur go fix présente le nouveau framework.
