Retour au blog

10 erreurs courantes en Go (Golang) que tout développeur devrait éviter

Découvrez les 10 erreurs de programmation les plus courantes en Go (Golang) et comment les éviter : gestion des octets dans les strings, append sur une slice, valeurs zéro des maps, enveloppement des erreurs, récepteurs par valeur, defer dans les boucles, channels non bufferisés, et plus encore.

10 erreurs courantes en Go (Golang) que tout développeur devrait éviter

Go, souvent recherché sous le nom de Golang, est un langage compact, et c'est justement pour cela qu'on le sous-estime facilement. Des développeurs qui pratiquent Go depuis des années tombent encore dans les mêmes quelques pièges. La plupart de ces bugs compilent sans problème. Le programme renvoie simplement un mauvais résultat, laisse fuir des ressources ou se bloque en deadlock en production.

Voici les 10 plus courants. Chacun est accompagné d'un exemple Golang exécutable et de la correction idiomatique.

Table des matières


1. Traiter une string comme des caractères plutôt que des octets

En Go, une string est une slice d'octets en lecture seule, pas une suite de caractères. len(s) renvoie le nombre d'octets, et l'indexation s[i] renvoie un octet. Pour du texte ASCII, cela ne pose aucun problème. Tout se casse dès qu'un caractère occupe plus d'un octet.

example.gogo
package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    word := "kött"
    fmt.Println("len:", len(word))
    fmt.Println("runes:", utf8.RuneCountInString(word))
}

Le mot suédois qui signifie « viande » compte quatre caractères mais cinq octets. Découper une string par index d'octet peut couper une rune en deux et produire de l'UTF-8 invalide.

Correction : utilisez utf8.RuneCountInString pour compter les caractères. Pour itérer, for i, r := range s vous donne des runes. Pour découper, convertissez d'abord en []rune.

2. Oublier de réaffecter le résultat de append

append ne met pas à jour la slice que vous lui passez. Quand la capacité est épuisée, il alloue un nouveau tableau sous-jacent et renvoie une slice qui pointe vers celui-ci. Si vous ignorez la valeur de retour, les éléments ajoutés sont perdus.

example.gogo
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3}
    appendWrong(nums)
    fmt.Println("wrong:", nums)

    nums = appendRight(nums)
    fmt.Println("right:", nums)
}

func appendWrong(s []int) {
    _ = append(s, 4)
}

func appendRight(s []int) []int {
    return append(s, 4)
}

Un appel isolé append(s, 4) est en réalité une erreur de compilation en Go, car la valeur de retour doit être utilisée. Le _ = dans appendWrong ne sert qu'à contourner le compilateur. Le comportement est le même que si vous appeliez append sans jamais réaffecter le résultat.

Correction : réaffectez toujours avec s = append(s, x). Si une fonction utilitaire agrandit une slice, faites-lui renvoyer la nouvelle slice et réaffectez-la à l'endroit de l'appel.

3. Les valeurs zéro qui masquent les clés absentes d'une map

Lire une clé absente dans une map ne renvoie pas d'erreur. Cela renvoie la valeur zéro du type des valeurs de la map. Pour une map[string]int, c'est 0, impossible à distinguer d'une clé volontairement définie à zéro.

example.gogo
package main

import "fmt"

func main() {
    scores := map[string]int{"alice": 0}

    if v := scores["alice"]; v == 0 {
        fmt.Println("alice looks the same as missing users")
    }

    if v, ok := scores["bob"]; ok {
        fmt.Println("bob:", v)
    } else {
        fmt.Println("bob is missing")
    }
}

Correction : utilisez l'idiome comma-ok, v, ok := m[key]. Le booléen vous indique si la clé existait.

4. Utiliser %v au lieu de %w pour envelopper une erreur

fmt.Errorf avec %v formate l'erreur d'origine dans une nouvelle string et jette la valeur de l'erreur. Avec %w, la nouvelle erreur enveloppe l'originale, si bien que errors.Is et errors.As peuvent toujours la retrouver.

example.gogo
package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    _, err := os.Open("does-not-exist.txt")
    wrapped := fmt.Errorf("reading config: %w", err)

    fmt.Println(wrapped)
    fmt.Println("is not-exist:", errors.Is(wrapped, os.ErrNotExist))
}

Correction : enveloppez avec %w dès que l'appelant pourrait avoir besoin de vérifier l'erreur sous-jacente. Réservez %v aux cas où l'erreur d'origine est un détail d'implémentation dont vous ne voulez pas que l'appelant dépende.

5. Les récepteurs par valeur qui perdent les modifications sans prévenir

Une méthode avec un récepteur par valeur reçoit une copie du struct. Tout ce qu'elle modifie sur cette copie disparaît au retour de la méthode.

example.gogo
package main

import "fmt"

type Counter struct {
    n int
}

func (c Counter) IncValue() {
    c.n++
}

func (c *Counter) IncPointer() {
    c.n++
}

func main() {
    c := Counter{}
    c.IncValue()
    fmt.Println("after value receiver:", c.n)

    c.IncPointer()
    fmt.Println("after pointer receiver:", c.n)
}

Correction : utilisez un récepteur pointeur quand la méthode modifie le récepteur ou quand le struct est assez gros pour que sa copie soit coûteuse. Si certaines méthodes d'un type ont besoin d'un récepteur pointeur, il est généralement plus propre de leur en donner un à toutes.

6. Les variables masquées (shadowing) qui renvoient une erreur nil

Une déclaration courte de variable (:=) à l'intérieur d'un if crée une nouvelle variable qui n'existe que dans ce bloc. Si elle masque par accident un err extérieur, celui-ci reste nil et la fonction signale un succès alors que quelque chose a échoué.

example.gogo
func loadUser(id string) (*User, error) {
    var err error
    user, err := fetch(id)
    if err != nil {
        return nil, err
    }

    if err := validate(user); err != nil {
        log.Println(err)
        // the outer err is still nil here
    }

    return user, err
}

Correction : affectez la variable extérieure avec = au lieu de :=, ou faites un return directement dans le bloc if. Activez go vet -shadow ou le linter govet pour détecter ces cas avant la code review.

7. Les champs de struct non exportés qui disparaissent du JSON

encoding/json ne voit que les champs exportés. Un champ en minuscules est invisible pour le marshaller, il est donc omis de la sortie sans la moindre erreur.

example.gogo
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    email string
    Age   int    `json:"age,omitempty"`
}

func main() {
    u := User{Name: "Ada", email: "[email protected]", Age: 36}
    data, _ := json.Marshal(u)
    fmt.Println(string(data))
}

La sortie ne contient pas de clé email, et rien ne vous en avertit.

Correction : mettez une majuscule aux champs que vous voulez sérialiser et utilisez les struct tags pour définir le nom de la clé JSON. Ajoutez omitempty aux champs optionnels.

8. Utiliser defer dans une boucle

defer s'exécute au retour de la fonction englobante, pas à la fin de chaque itération de la boucle. Si une boucle ouvre mille fichiers et diffère file.Close() pour chacun, les mille descripteurs de fichiers restent ouverts jusqu'à la sortie de la fonction.

example.gogo
func processAll(paths []string) error {
    for _, p := range paths {
        f, err := os.Open(p)
        if err != nil {
            return err
        }
        defer f.Close() // all closes pile up until processAll returns
        // ... read file ...
    }
    return nil
}

Correction : déplacez le travail de chaque itération dans une fonction utilitaire, afin que defer s'exécute à la fin de chaque appel.

example.gogo
func processAll(paths []string) error {
    for _, p := range paths {
        if err := processOne(p); err != nil {
            return err
        }
    }
    return nil
}

func processOne(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()
    // ... read file ...
    return nil
}

9. Ignorer les erreurs avec l'identifiant vide

result, _ := doThing() jette tout échec signalé par la fonction. Le programme continue avec une valeur zéro ou un état à moitié construit, et quand il finit par planter, c'est loin de la vraie cause.

example.gogo
data, _ := os.ReadFile("config.json")
_ = json.Unmarshal(data, &cfg)

Si le fichier est absent, data vaut nil, Unmarshal renvoie une erreur que personne ne lit, et cfg reste à zéro. Vous le découvrez plus tard, quand l'application se comporte comme s'il n'y avait aucune configuration.

Correction : traitez l'erreur ou renvoyez-la à l'appelant. N'utilisez _ que lorsque l'erreur ne peut vraiment avoir aucune importance, et laissez un court commentaire qui explique pourquoi.

10. Les deadlocks causés par les channels non bufferisés

Sur un channel non bufferisé, un envoi ne se termine que lorsqu'un récepteur prend la valeur au même moment. Si rien ne reçoit jamais, l'envoi bloque indéfiniment.

example.gogo
package main

import "fmt"

func main() {
    ch := make(chan int) // unbuffered
    ch <- 42             // deadlock: no goroutine is receiving
    fmt.Println(<-ch)
}

L'envoi attend un récepteur, mais la réception se trouve à la ligne suivante et ne s'exécute jamais. Le runtime constate que toutes les goroutines sont bloquées et s'arrête avec fatal error: all goroutines are asleep - deadlock!.

Correction : assurez-vous qu'une goroutine est prête à recevoir avant d'envoyer, ou utilisez un channel bufferisé quand le producteur et le consommateur avancent à des rythmes différents. Pour un résultat unique, un channel avec un buffer de 1 est un choix par défaut sûr.

example.gogo
package main

import "fmt"

func main() {
    ch := make(chan int, 1) // buffered
    ch <- 42
    fmt.Println(<-ch)
}

Pour aller plus loin

Connaître ces dix pièges Golang vous épargnera beaucoup de débogage. Le reste s'apprend en construisant de vrais projets et en tombant sur des cas limites, un débogueur ouvert. Si vous voulez une pratique structurée, le parcours Go de LevelUpGo couvre ces patterns avec des exercices exécutables et un retour automatisé sur votre code.

É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