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
- 2. Oublier de réaffecter le résultat de append
- 3. Les valeurs zéro qui masquent les clés absentes d'une map
- 4. Utiliser %v au lieu de %w pour envelopper une erreur
- 5. Les récepteurs par valeur qui perdent les modifications sans prévenir
- 6. Les variables masquées (shadowing) qui renvoient une erreur nil
- 7. Les champs de struct non exportés qui disparaissent du JSON
- 8. Utiliser defer dans une boucle
- 9. Ignorer les erreurs avec l'identifiant vide
- 10. Les deadlocks causés par les channels non bufferisés
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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogopackage 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.gogofunc 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.gogopackage 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.gogofunc 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.gogofunc 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.gogodata, _ := 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.gogopackage 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.gogopackage 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.
