Go : Référence d'efficacité idiomatique
Table des matières
- Gestion des erreurs
- Slices et Maps
- Goroutines et Canaux
- Structs et Interfaces
- Fonctions et Fermetures
- Anti-patrons spécifiques à Go
1. Gestion des erreurs {#errors}
// ❌ Ignorer les erreurs
result, _ := os.Open(path)
// ✅ — toujours gérer ; n'utiliser _ que lorsque l'erreur est prouvée sans conséquence
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("ouverture %s : %w", path, err)
}
// ❌ Variable d'erreur redondante
err := doA()
if err != nil { return err }
err = doB()
if err != nil { return err }
// ✅ — chaque := / = est correct ; c'est du Go idiomatique. N'essayez pas de le « corriger ».
// Ce que vous POUVEZ simplifier : réduire à une seule ligne quand le corps du if est un simple return
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ Type d'erreur personnalisé sans valeur ajoutée
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — utiliser errors.New ou fmt.Errorf sauf si les appelants ont besoin d'inspecter le type
var ErrNotFound = errors.New("non trouvé")
return fmt.Errorf("recherche %q : %w", key, ErrNotFound)
Enveloppez les erreurs avec %w (pas %v) pour que les appelants puissent utiliser errors.Is / errors.As.
2. Slices et Maps {#slices}
// ❌ Agrandir une slice sans pré-allocation quand la taille est connue
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ Vérification manuelle d'existence avant écriture dans une map
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — append sur une slice nil est valide en Go
m[key] = append(m[key], value)
// ❌ Copier une map par affectation (copie la référence)
copy := original
// ✅
copy := make(map[K]V, len(original))
for k, v := range original { copy[k] = v }
3. Goroutines et Canaux {#concurrency}
// ❌ Goroutine « fire-and-forget » sans cycle de vie
go doWork()
// ✅ — utiliser errgroup ou WaitGroup pour suivre l'achèvement
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ Canal non tamponné bloquant une goroutine inutilement
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — pour un seul résultat, un canal tamponné évite la fuite de goroutine si le receveur se ferme tôt
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ select avec une attente active par défaut
for {
select {
case v := <-ch:
process(v)
default:
// boucle
}
}
// ✅ — select bloquant sauf si vous avez vraiment besoin de non-bloquant
for v := range ch {
process(v)
}
Utilisez golang.org/x/sync/errgroup pour le fan-out avec collecte d'erreurs.
4. Structs et Interfaces {#structs}
// ❌ Grande interface
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... 10 méthodes supplémentaires
}
// ✅ — interfaces petites et composables
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ Retourner une structure concrète depuis un constructeur (lie les appelants à l'implémentation)
func NewStore() *RedisStore { ... }
// ✅ — retourner une interface lorsque vous avez ou anticipez plusieurs implémentations
func NewStore() Storage { return &RedisStore{...} }
// ❌ Récepteur pointeur pour de petits types de valeur
func (p *Point) X() float64 { return p.x }
// ✅ — récepteur valeur pour les petits types immuables
func (p Point) X() float64 { return p.x }
Règle : récepteur pointeur lorsque la méthode modifie l'état OU que la structure est grande (>3 champs de taille non triviale). Récepteur valeur sinon.
5. Fonctions et Fermetures {#functions}
// ❌ Valeurs de retour nommées utilisées uniquement pour éviter une déclaration de variable
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — les retours nommés n'en valent la peine que pour une mutation différée ou la documentation
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division par zéro") }
return a / b, nil
}
// ❌ Fermeture capturant la variable de boucle (bug classique en Go, corrigé dans Go 1.22+)
// Avant 1.22 : chaque goroutine capture la même i
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — passer en paramètre)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+ : variable de boucle avec sa propre portée par itération, donc l'original est sûr
6. Anti-patrons spécifiques à Go {#antipatterns}
| Anti-patron | Préféré |
|---|---|
if err != nil { return err } répété 5 fois ou plus | acceptable — c'est du Go idiomatique |
panic pour les erreurs attendues | return err |
init() avec effets de bord | initialisation explicite dans main ou les constructeurs |
interface{} / any sans génériques | utiliser les génériques (Go 1.18+) ou des interfaces typées |
| Champ Mutex non adjacent aux données qu'il protège | placer mu juste au-dessus du champ qu'il protège |
| Canal de canaux | généralement un signe de sur-ingénierie ; reconcevoir |
time.Sleep dans les tests | utiliser des hooks de testing ou des canaux pour la synchronisation |
| Types exportés avec champs non exportés (quand les champs sont le sujet principal) | structures de type record avec tous les champs exportés |
log.Fatal en dehors de main | remonter les erreurs dans la pile |
Limitations
- Ces directives sont spécifiques au langage et ne couvrent pas les décisions architecturales globales.
- Une sur-compression pourrait réduire la lisibilité ; faites preuve de jugement.