Retour au blog

Tous les modernizers de `go fix` dans Go 1.26

Go 1.26 a reconstruit go fix sur le framework d'analyse. Voici tous les modernizers qu'il embarque : 26 diffs avant/après couvrant le langage et la stdlib.

Tous les modernizers de `go fix` dans Go 1.26

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.bashbash
go 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

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.gogo
func log(args ...interface{}) {
    fmt.Println(args...)
}

var cache map[string]interface{}

Après :

example.gogo
func 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.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

Après :

example.gogo
x := 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.gogo
for i := 0; i < n; i++ {
    fmt.Println(i)
}

Après :

example.gogo
for 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.gogo
for _, x := range items {
    x := x // pre-1.22 capture trick
    go func() { use(x) }()
}

Après :

example.gogo
for _, 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.gogo
func strPtr(s string) *string {
    p := s
    return &p
}

cfg.Name = strPtr("LevelUpGo")

Après :

example.gogo
cfg.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.gogo
sort.Slice(s, func(i, j int) bool {
    return s[i] < s[j]
})

Après :

example.gogo
slices.Sort(s)

slicescontains (Go 1.21) : les boucles de recherche manuelles deviennent slices.Contains

Avant :

example.gogo
func has(s []int, x int) bool {
    for _, v := range s {
        if v == x {
            return true
        }
    }
    return false
}

Après :

example.gogo
func 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.gogo
for i := len(s) - 1; i >= 0; i-- {
    use(i, s[i])
}

Après :

example.gogo
for 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.gogo
func 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.gogo
func 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.gogo
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
for _, k := range keys {
    use(k)
}

Après :

example.gogo
for 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.gogo
t := reflect.TypeOf((*MyType)(nil)).Elem()

Après :

example.gogo
t := 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.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitempty"`
}

Après :

example.gogo
type 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.gogo
b := []byte(fmt.Sprintf("%s=%d", key, val))

Après :

example.gogo
b := 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.gogo
i := strings.Index(s, "=")
if i >= 0 {
    key, val := s[:i], s[i+1:]
    use(key, val)
}

Après :

example.gogo
if 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.gogo
if strings.HasPrefix(s, "go-") {
    rest := strings.TrimPrefix(s, "go-")
    handle(rest)
}

Après :

example.gogo
if 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.gogo
for _, line := range strings.Split(s, "\n") {
    handle(line)
}

Après :

example.gogo
for 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.gogo
s := ""
for _, p := range parts {
    s += p
}
return s

Après :

example.gogo
var 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.gogo
addr := fmt.Sprintf("%s:%d", host, port)
net.Dial("tcp", addr)

Après :

example.gogo
addr := 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.gogo
var myerr *MyErr
if errors.As(err, &myerr) {
    handle(myerr)
}

Après :

example.gogo
if 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.gogo
p := unsafe.Pointer(uintptr(base) + uintptr(off))

Après :

example.gogo
p := 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.gogo
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("go!")
}()
wg.Wait()

Après :

example.gogo
var 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.gogo
var count int64
atomic.AddInt64(&count, 1)
n := atomic.LoadInt64(&count)

Après :

example.gogo
var 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.gogo
func TestThing(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    work(ctx)
}

Après :

example.gogo
func 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.gogo
func BenchmarkThing(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        work()
    }
}

Après :

example.gogo
func 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 :

  1. Partez d'une branche propre, sans autre modification en cours.
  2. Lancez d'abord go fix -diff ./.... Ce flag affiche un diff unifié sur la sortie standard au lieu d'écrire les fichiers. Parcourez-le.
  3. Lancez go fix ./... pour appliquer les changements.
  4. Lancez git diff sur les modifications réelles. Repérez toute réécriture qui vous surprend, en particulier dans du code volontairement écrit dans un style plus ancien.
  5. Exécutez vos tests, votre linter et go build ./....
  6. 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.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

La première passe réécrit la borne inférieure en max :

example.gogo
x := max(0, f())
if x > 100 {
    x = 100
}

La deuxième passe voit le motif if/affectation restant et termine le travail :

example.gogo
x := 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 si go.mod déclare la bonne version minimale. Si vous ne voyez pas la réécriture attendue, vérifiez votre directive go.
  • Pour les migrations propres à votre base de code, utilisez la directive //go:fix inline. Marquez un wrapper ou une constante, lancez go 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.

É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