Québec, Canada

403-1381 1re Avenue

+1 581.849.27.96

bdgouthiere@gmail.com

Tester la concurrence en Go : le flag -race et les data races que l'IA te glisse dans le dos

Ou : Pourquoi Go est le seul langage mainstream à livrer un détecteur de bugs concurrents en standard, et pourquoi tu ne t’en sers probablement pas

Une API de gestion de panier. Un map[string]int qui compte les articles par utilisateur. Deux handlers HTTP qui incrémentent ce compteur quand une requête arrive. Pas de mutex — le dev qui a écrit le code (ou l’IA qui l’a généré) s’est dit qu’un count++ était une opération « trop petite » pour avoir besoin de synchronisation. Les tests passent. La CI est verte. Le déploiement se fait. Et pendant trois semaines, en production, le compteur donne occasionnellement des valeurs impossibles — des -1, des 4 milliards, parfois un panic fatal qui fait tomber le pod. Aucun log ne pointe vers la cause. Aucun test unitaire ne la reproduit.

Cette histoire existe dans à peu près tous les repos Go qui ont plus de deux ans. Ce qui est spécifique à Go, c’est qu’elle ne devrait pas exister, parce que Go livre depuis 2013 un outil intégré qui l’attrape en moins d’une seconde — à condition qu’un test l’exerce. Le flag -race du runtime Go, branché sur le ThreadSanitizer de LLVM, instrumente chaque lecture et écriture mémoire pendant les tests et signale toute paire d’accès non synchronisés sur la même adresse. C’est l’outil qui fait la différence entre « ce code compile » et « ce code survivra à la concurrence ». Et c’est celui que l’IA oublie systématiquement d’invoquer dans ses Makefiles générés.

Dans les dix articles précédents de cette série, on a couvert les fondamentaux du testing Go : table-driven, subtests, httptest, mocks, Testify, fuzzing, benchmarks, intégration Testcontainers, code coverage. Il reste un sujet transversal que la concurrence ajoute à chaque itération précédente — et qui, paradoxalement, est aussi le sujet le plus trivialement outillé de la stdlib Go.


Tester la concurrence en Go : data races, -race, t.Parallel() et les patterns qui font tomber la prod

D’abord, un mot sur le vocabulaire : data race ≠ race condition

Presque tout le monde utilise les deux termes interchangeablement, et presque tout le monde a tort. La distinction vaut trente secondes de lecture parce qu’elle explique une bonne partie de ce que -race voit et ne voit pas.

Une data race est un phénomène mémoire : deux goroutines accèdent à la même adresse, au moins l’une des deux écrit, et aucune relation happens-before ne les ordonne. C’est une propriété locale, observable, et détectable mécaniquement par un outil qui regarde les accès mémoire.

Une race condition est un phénomène de correction : le résultat du programme dépend de l’ordre d’exécution de choses qui, logiquement, ne devraient pas se chevaucher. C’est une propriété sémantique, qui dépend de ce que le programme est censé faire.

Toute data race est une race condition, mais l’inverse est faux1. Tu peux avoir un programme parfaitement synchronisé au niveau mémoire (tous les accès partagés passent par un mutex ou un channel) qui reste une race condition au niveau métier — un grand classique étant le check-then-act sur une map : tu lis la valeur sous mutex, tu relâches le mutex, tu décides quoi faire en fonction, et entre-temps une autre goroutine a tout changé. Aucune data race, logique cassée quand même.

Ce que -race attrape, c’est exclusivement les data races. C’est à la fois moins que ce que tu voudrais (il ne comprend rien à ta logique métier) et exactement ce dont tu as besoin : les data races sont le sous-ensemble des bugs concurrents qui sont mécaniquement détectables, et c’est déjà une énorme victoire. Les race conditions métier, elles, restent à ta charge — avec des tests qui assertent sur les états finaux, pas sur les accès mémoire.

Comment -race fonctionne réellement

Quand tu lances go test -race, le compilateur Go insère une instrumentation autour de chaque opération mémoire : avant chaque lecture et avant chaque écriture, le runtime note l’adresse, la goroutine courante, et un vector clock qui permet de reconstruire la relation happens-before entre les événements. Cette instrumentation est fournie par ThreadSanitizer (TSan), une bibliothèque runtime développée à l’origine chez Google pour C++, maintenue aujourd’hui dans le projet LLVM, et intégrée à Go en septembre 2012 par Dmitry Vyukov2. Le flag -race est officiellement sorti avec Go 1.1 le 13 mai 2013 et l’article fondateur sur le blog Go officiel date du 26 juin 2013.

Le mécanisme, en une phrase : TSan maintient pour chaque adresse mémoire la trace du dernier accès (qui, quelle goroutine, à quel instant logique) et signale toute paire d’accès où au moins l’un est une écriture et où il n’existe pas de chaîne synchronized-before entre eux. Les primitives de synchronisation Go — sync.Mutex.Lock/Unlock, sync.WaitGroup.Wait, envoi/réception sur un chan, atomic.AddInt32 — établissent explicitement cette relation, ce qui permet au détecteur de les distinguer d’un accès concurrent pur.

Le coût est réel et il faut le connaître : programmes compilés avec -race tournent typiquement 2 à 20 fois plus lentement et consomment 5 à 10 fois plus de mémoire3. C’est pour ça qu’on ne compile pas son binaire de production avec -race — mais ce sont des chiffres parfaitement acceptables pour une CI, où la correction compte plus que le débit. La forme canonique à graver dans ton Makefile :

1
go test -race -covermode=atomic -coverprofile=coverage.out ./...

Note le -covermode=atomic — il va avec -race parce que le mode par défaut de la coverage utilise des compteurs non atomiques qui produiraient eux-mêmes des data races sous parallélisation. On en a parlé dans l’article précédent sur la coverage.

Écrire un test qui déclenche volontairement une data race

Le vrai test de concurrence, ce n’est pas « est-ce que ma fonction marche quand elle est appelée une fois » — c’est « est-ce que la mise sous pression simultanée expose un accès non synchronisé ». Il faut donc construire explicitement le scénario.

Voici un bug classique que l’IA génère régulièrement — un compteur d’événements en mémoire sans synchronisation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// counter.go
type Counter struct {
    values map[string]int
}

func NewCounter() *Counter {
    return &Counter{values: make(map[string]int)}
}

func (c *Counter) Inc(key string) {
    c.values[key]++
}

func (c *Counter) Get(key string) int {
    return c.values[key]
}

Un test unitaire séquentiel passe sans problème. Il ne trouve rien, parce que rien n’accède concurremment. Pour forcer la data race à se manifester, on lance des goroutines en parallèle sur la même instance :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// counter_test.go
func TestCounter_Concurrent(t *testing.T) {
    c := NewCounter()
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Inc("requests")
        }()
    }
    wg.Wait()

    // Assertion métier : on attend 100 incréments.
    if got := c.Get("requests"); got != 100 {
        t.Errorf("want 100, got %d", got)
    }
}

Lancé en go test, ce test peut passer ou échouer aléatoirement selon le scheduler — et c’est précisément le genre de flakiness qu’un développeur ajoute à une liste de « tests connus pour être instables, skippez-les en CI ». Lancé en go test -race, il échoue immédiatement avec une sortie qui ressemble à :

==================
WARNING: DATA RACE
Read at 0x00c000100018 by goroutine 8:
  runtime.mapaccess1_faststr()
      /usr/local/go/src/runtime/map_faststr.go:13 +0x0
  main.(*Counter).Inc()
      /path/counter.go:13 +0x4c
  ...

Previous write at 0x00c000100018 by goroutine 7:
  runtime.mapassign_faststr()
      /usr/local/go/src/runtime/map_faststr.go:203 +0x0
  main.(*Counter).Inc()
      /path/counter.go:13 +0x80
  ...
==================

TSan te donne : l’adresse concernée, la goroutine qui lit, la goroutine qui écrit, les stack traces des deux accès, et une indication du synchronized-before manquant. C’est typiquement une information qui prend trente secondes à lire et qui aurait demandé des jours de bisection sans l’outil. La correction est triviale — ajouter un sync.Mutex ou utiliser sync.Map — mais c’est la détection qui est la valeur ajoutée.

Le pattern général à graver dans la tête : un test de concurrence utile lance plusieurs goroutines qui exercent simultanément l’API sous test. Un seul appel séquentiel, même répété cent fois dans une boucle, ne révèle rien que le détecteur puisse voir. Il faut la simultanéité, garantie par go + sync.WaitGroup, pour que TSan ait quelque chose à instrumenter.

Les patterns qui reviennent : l’étude Uber PLDI 2022

Si tu te demandes quels sont les patterns de data races les plus fréquents dans du vrai code Go, il existe maintenant une réponse chiffrée. En 2022, Milind Chabbi (Uber) a publié à PLDI une étude sur le déploiement d’un détecteur de data races sur la codebase Uber : 46 millions de lignes de Go, 2 100 microservices, plus de 2 000 data races détectées et plus de 1 000 corrigées sur 6 mois par 210 développeurs4. C’est, à ma connaissance, la plus grande étude empirique existante sur les data races en production Go.

Les deux patterns numéro 1 et 2 du paper méritent d’être connus parce qu’ils sont à la fois triviaux à écrire et invisibles en review :

Pattern 1 — Loop variable capture. Tu itères sur un slice et tu lances une goroutine par élément :

1
2
3
4
5
6
// Code dangereux avant Go 1.22
for _, item := range items {
    go func() {
        process(item)  // `item` capture la variable de boucle par référence
    }()
}

Toutes les goroutines capturent la même variable item, qui change à chaque itération. Selon le scheduling, tu peux avoir toutes les goroutines qui traitent le dernier élément, ou un mélange aléatoire. L’IA a généré ce bug dans à peu près tout ce qu’elle a écrit entre 2021 et 2024.

Bonne nouvelle pour Go 2026 : ce piège a été résolu au niveau du langage. Depuis Go 1.22 (février 2024), la spec a changé — chaque itération de for range crée sa propre variable, et la capture se fait proprement5. Si ton projet est sur Go 1.22 ou plus récent, ce pattern est automatiquement corrigé. Si tu maintiens du code plus ancien, le fix historique consiste à rebinder explicitement :

1
2
3
4
5
6
for _, item := range items {
    item := item  // rebinding explicite, Go < 1.22
    go func() {
        process(item)
    }()
}

Ou plus propre, passer en paramètre :

1
2
3
4
5
for _, item := range items {
    go func(item T) {
        process(item)
    }(item)
}

Pattern 2 — Closure variable capture. Même logique, mais sur n’importe quelle variable locale capturée par une goroutine sans réaliser que Go capture par référence. L’IA, qui génère des closures comme elle respire, se fait régulièrement piéger par ça.

Le point intéressant de l’étude Uber, ce n’est pas la liste des patterns — c’est leur distribution. Les deux premiers patterns concernent la capture de variables de boucle et de closure, les deux mécanismes les plus idiomatiques de Go. Les bugs les plus fréquents ne viennent pas de l’exotique, ils viennent du code qui a l’air correct. C’est exactement le type de code que l’IA produit en volume.

Le pattern de loop variable capture — résolu au niveau du langage en Go 1.22, encore présent dans tout code plus ancien et dans tout ce que l'IA a appris sur les snippets d'avant 2024.

t.Parallel() + -race : le combo qui multiplie la puissance de détection

t.Parallel() déclare qu’un test peut s’exécuter en parallèle avec d’autres tests qui ont fait la même déclaration. Utilisé seul, c’est un outil d’optimisation — ta suite de tests tourne plus vite. Combiné à -race, il devient un outil de détection : il maximise les chances que deux tests touchent simultanément à un même état partagé, et donc que TSan voie la race si elle existe.

1
2
3
4
5
6
7
8
9
func TestHandlerA(t *testing.T) {
    t.Parallel()
    // ...
}

func TestHandlerB(t *testing.T) {
    t.Parallel()
    // ...
}

Si ces deux tests utilisent une variable globale (singleton, cache partagé, compteur de métrique…) sans synchronisation, go test -race va l’attraper au premier run sur une machine avec plus d’un cœur. Sans t.Parallel(), les deux tests s’exécutent en séquence dans la même goroutine, et la race ne se matérialise jamais.

Règle opérationnelle : ajoute t.Parallel() à tous tes tests qui n’ont pas de raison positive de ne pas l’avoir, et lance systématiquement -race. Le gain est double — ta suite est plus rapide, et ta couverture de détection des data races augmente mécaniquement. Les tests qui ne peuvent pas être parallélisés (typiquement ceux qui manipulent une variable globale, un singleton, ou l’état du FS) deviennent visibles par exception — et c’est précisément les endroits où tu veux regarder de plus près.

Attention toutefois : si deux tests parallèles partagent de l’état volontairement (fixture globale), tu devras explicitement synchroniser cet état, sans quoi -race te signalera une race légitime. C’est en général le signe que ta fixture devrait être par-test plutôt que globale, mais c’est à toi de juger. Le détecteur ne te juge pas, il te signale.

Les trois outils de synchronisation (et quand utiliser quoi)

Quand -race signale une data race, la correction passe par une primitive de synchronisation. Go en offre trois principales, et le choix entre elles n’est ni aléatoire ni esthétique.

sync.Mutex / sync.RWMutex — pour protéger une structure de données partagée en lecture/écriture. C’est l’outil pour « cette map / ce slice est lu et écrit par plusieurs goroutines, je veux qu’un seul thread y accède à la fois ». RWMutex te permet de paralléliser les lectures si elles sont très dominantes, mais n’utilise jamais RWMutex par défaut — son overhead sur les petites sections critiques est plus élevé qu’un Mutex simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Counter struct {
    mu     sync.Mutex
    values map[string]int
}

func (c *Counter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.values[key]++
}

sync.WaitGroup — pour attendre qu’un ensemble de goroutines se termine, sans rien échanger d’autre. C’est l’outil pour orchestrer des tests concurrents et pour synchroniser un producteur avec des workers.

chan — pour transmettre de la donnée entre goroutines avec une relation happens-before garantie (envoi < réception en termes synchronized-before). L’idiome Go « don’t communicate by sharing memory, share memory by communicating »6 pointe vers les channels comme solution par défaut — mais c’est un conseil, pas une loi. Pour un compteur d’événements, un sync.Mutex est plus simple, plus lisible et plus rapide qu’une orchestration à base de channels. Les channels brillent quand tu veux modéliser un flux (pipeline de traitement, fan-out/fan-in, signal de shutdown), pas pour sérialiser des accès mémoire.

La règle que je recommande : mutex pour protéger un état, channels pour faire circuler un flux, WaitGroup pour synchroniser la fin d’un lot. Si tu es tenté d’utiliser un channel pour protéger un compteur, demande-toi si un mutex ne serait pas plus honnête.

Les limites de -race : ce qu’il ne voit pas

Le détecteur est remarquable, il n’est pas parfait. Trois limites à garder en tête :

Il ne voit que les accès effectivement exécutés. Si ton test ne lance pas la goroutine buggée, TSan n’a rien à détecter. Le détecteur est dynamique, pas statique — c’est la contrepartie de son absence de faux positifs7. D’où l’importance de tests qui exercent activement la concurrence, pas simplement d’un go test -race sur des tests séquentiels.

Il peut avoir des faux négatifs sous très forte charge. TSan maintient un history buffer limité (4 millions d’accès mémoire en configuration maximale). Au-delà, les anciens accès sont écrasés, et une race entre un très vieil accès et un nouvel accès peut être ratée. C’est rare en pratique, mais documenté dans la page officielle.

Il ne détecte pas les race conditions métier. Un programme parfaitement data-race-free peut avoir une logique cassée par le non-déterminisme d’ordonnancement (le check-then-act évoqué plus haut, ou deux services qui s’écrivent dans le dos). Pour ces bugs, -race est aveugle — c’est ton asserion métier qui doit les attraper, éventuellement avec du fuzzing ou des tests de propriétés.

Ces limites étant dites, -race reste l’outil avec le meilleur ratio signal/bruit de toute la toolchain Go. Zéro faux positif, documenté au niveau de la spec. Quand il te signale une race, il y a une race. C’est une garantie rare en outillage statique/dynamique, et elle est la raison pour laquelle tu peux — et dois — lui faire confiance assez pour faire échouer ton CI sur ses avertissements.


Dialogue : « Mais le test passe quand je le relance »

Junior Jules : Le test TestCounter_Concurrent échoue en CI avec un warning TSan. Chez moi il passe.

Senior Sam : Tu le lances avec -race en local ?

Junior Jules : Euh… non. Juste go test.

Senior Sam : Donc il ne passe pas chez toi — il passe silencieusement. C’est différent. Le test touche à une map partagée depuis plusieurs goroutines sans mutex. Sans -race, Go ne dit rien tant que la corruption n’est pas assez grave pour paniquer.

Junior Jules : Mais ça a toujours marché avant !

Senior Sam : Ça a toujours marché assez bien. La différence entre « data race qui corrompt occasionnellement en prod » et « data race qui fait panic au premier test » est uniquement le scheduler du runtime. Tu joues à la loterie à chaque déploiement. TSan te dit exactement ce que le scheduler finira par te faire — juste plus tôt, et dans un environnement où tu peux lire la stack.

Junior Jules : OK. Je mets un mutex ?

Senior Sam : Tu mets un mutex. Tu relances go test -race, tu vérifies qu’il passe, et tu ajoutes -race à la cible test du Makefile pour qu’on ne se repose jamais la question. Pendant que tu y es, ajoute t.Parallel() à tes tests qui peuvent l’être, tu augmentes la pression sur TSan gratuitement.

Junior Jules : Et si une goroutine de l’IA crée un nouveau bug demain ?

Senior Sam : -race l’attrapera. C’est exactement son métier. Le tien, c’est de l’avoir branché dans la CI pour qu’il ait une chance de le voir.


Ce que tu peux faire maintenant

  1. Ajoute -race à la cible test de ton Makefile si ce n’est pas déjà fait. Coût : 2-20x sur la durée de la CI, typiquement invisible sur des suites de quelques secondes à quelques minutes. Bénéfice : zéro data race en prod, garantie.
  2. Combine -race avec -covermode=atomic quand tu génères aussi un rapport de coverage — sans quoi tes compteurs de coverage sont eux-mêmes sources de data races.
  3. Écris au moins un test qui exerce ta concurrence pour chaque type avec état partagé. 100 goroutines qui appellent la même méthode, un sync.WaitGroup à la fin, une assertion sur l’état final. Ça tient en 15 lignes et ça attrape les bugs d’une année de refactor.
  4. Ajoute t.Parallel() à tout test qui n’a pas de raison positive de ne pas l’avoir. Ta suite tourne plus vite, ta détection de races monte mécaniquement. Les tests qui ne peuvent pas être parallélisés deviennent visibles par exception et c’est là que sont les singletons à éviter.
  5. Quand TSan signale une race, lis la stack. Les deux stacks complètes sont dans le warning. Tu sais exactement quelle goroutine lit, laquelle écrit, et où. La correction prend typiquement cinq minutes — contre des jours de bisection si tu avais attendu que la race se manifeste en prod.
  6. Ne lance jamais ton binaire de production compilé avec -race. C’est un outil de CI, pas de runtime. Le coût mémoire (5-10x) et CPU (2-20x) est acceptable pendant quelques minutes de test, pas pendant l’exécution continue d’un service.

La suite

Cet article clôt le tour d’horizon des outils de testing Go : table-driven, subtests, httptest, mocks, Testify, fuzzing, benchmarks, intégration Testcontainers, coverage, concurrence. Dix sujets, dix outils, une philosophie — tester ce que l’IA génère, pas juste le relire.

Il reste un dernier article dans la série : le capstone. On prend un vrai projet Go (une API REST avec base de données), et on applique tout ce qu’on a vu sur un seul repo — la pyramide de tests complète, les benchmarks ciblés, le coverage intelligent, la CI qui passe -race automatiquement. C’est l’article qui montre comment les dix précédents s’articulent dans un projet qu’on pourrait shipper demain.


  1. La distinction est développée en profondeur dans l’article de John Regehr Race Condition vs. Data Race, et la page Wikipedia sur la race condition contient une section dédiée qui cite un exemple canonique : un programme en atomic operations only (tous les accès partagés passent par des opérations atomiques, donc zéro data race) peut quand même avoir une race condition métier si la logique dépend de l’ordre. Go reconnaît explicitement cette distinction dans sa spec du memory model : « A data-race-free program executes as if all the goroutines were multiplexed onto a single processor » — autrement dit, l’absence de data race garantit la cohérence séquentielle (SC for DRF), mais pas la correction de ta logique. ↩︎

  2. Dmitry Vyukov est l’ingénieur Google qui a intégré ThreadSanitizer au runtime Go. La timeline officielle : intégration en septembre 2012, sortie publique avec Go 1.1 le 13 mai 2013, article d’annonce co-signé avec Andrew Gerrand publié le 26 juin 2013. ThreadSanitizer lui-même est un projet plus ancien — développé initialement à Google par Konstantin Serebryany, originalement comme un outil Valgrind-based, puis refait en instrumentation de compilateur dans LLVM/Clang vers 2011. Un papier de référence de Serebryany à WBIA 2009 documente la première version ; la version utilisée par Go aujourd’hui est TSanV2, compilée directement depuis les sources LLVM dans le repo google/sanitizers↩︎

  3. Les chiffres d’overhead viennent de la documentation officielle ThreadSanitizerGoManual sur le wiki google/sanitizers. Fourchette officielle : 2-20x CPU, 5-10x mémoire. La variance vient de ce que l’instrumentation est insérée à chaque accès mémoire — un programme très compute-bound voit peu de différence, un programme qui manipule beaucoup de petites allocations et d’accès à des maps voit un overhead important. Pour un service web typique, l’overhead en CI est généralement autour de 3-5x, ce qui reste acceptable pour tous les projets sauf les très gros monorepos. Le history_size par défaut couvre 32K accès mémoire ; la valeur maximale (history_size=7) couvre 4M accès et multiplie la consommation mémoire. ↩︎

  4. Le paper Uber s’appelle A Study of Real-World Data Races in Golang, publié à PLDI 2022 par Milind Chabbi et Murali Krishna Ramanathan. La version blog vulgarisée est un meilleur point d’entrée si tu veux juste la liste des patterns. Les sept observations qu’ils remontent valent la lecture complète, mais les deux plus fréquentes — loop capture et closure capture — représentent à elles seules une part substantielle des bugs. Uber a aussi publié depuis goleak, un détecteur de goroutine leaks, et un tooling interne appelé LeakProf pour la détection en production — signe que la concurrence Go en prod reste un chantier actif même chez les équipes les plus sophistiquées. ↩︎

  5. La release notes Go 1.22 documente le changement : « Previously, the variables declared by a “for” loop were created once and updated by each iteration. In Go 1.22, each iteration of the loop creates new variables ». Le changement a été discuté pendant plusieurs cycles (c’est une des rares modifications rétrocompatibles-cassantes de la spec Go) et a été validé après une analyse montrant que la grande majorité du code existant n’était pas affectée négativement, et qu’une fraction significative était corrigée silencieusement. C’est un rare exemple de fix de bug au niveau du langage qui élimine une classe entière d’erreurs humaines — et les 2000+ data races d’Uber contiennent probablement un gros volume de ce pattern, maintenant obsolète pour tout projet sur Go 1.22+. ↩︎

  6. La formule « Do not communicate by sharing memory; instead, share memory by communicating » est un idiome Go attribué à Rob Pike, popularisé dans le talk Go Concurrency Patterns et repris dans la documentation officielle du package sync. C’est un conseil, pas une règle — la page Go Wiki MutexOrChannel donne une liste de cas où le mutex est plus approprié : protéger un cache, compter des événements, protéger un état interne à un type. Les channels sont plus appropriés pour : passer la propriété d’une donnée d’une goroutine à une autre, distribuer une unité de travail, communiquer des résultats asynchrones. ↩︎

  7. La garantie officielle « no false positives » est documentée dans la FAQ du race detector et confirmée par Dmitry Vyukov sur les groupes de discussion golang-nuts : l’algorithme a été conçu pour préférer les faux négatifs aux faux positifs, précisément pour que les développeurs puissent faire confiance aux warnings sans avoir à les trier manuellement. En pratique cela signifie : un WARNING: DATA RACE est toujours un vrai bug, même quand le fix nécessaire semble cosmétique (par exemple un field qui est « logiquement » écrit une seule fois mais que le compilateur ne peut pas prouver séquentiel). Toujours corriger, jamais désactiver avec -count=1 ou skip. ↩︎