Envoyer et recevoir
Un channel est un tuyau typé entre goroutines. ch <- v envoie, <-ch reçoit. On en crée un avec make :
La valeur zéro d'un type channel est nil, donc var ch chan string sans make vous donne un channel qui bloque indéfiniment. Créez toujours vos channels avec make.
Les channels sans buffer synchronisent
make(chan T) crée un channel sans buffer. Un envoi bloque jusqu'à ce qu'un récepteur prenne la valeur, et une réception bloque jusqu'à ce qu'un émetteur en fournisse une. Les deux goroutines se rejoignent à ce point, ce qui fait d'un channel sans buffer un outil de synchronisation autant qu'un tuyau de données. Quand <-done retourne ci-dessous, vous savez que le worker a terminé tout ce qui précède son envoi :
Lire result dans main est sûr ici sans mutex. Le modèle mémoire de Go garantit que tout ce que le worker a fait avant l'envoi est visible par main après la réception correspondante. chan struct{} est le type idiomatique pour un simple signal, parce que struct{} n'occupe aucune mémoire.
Les channels avec buffer
make(chan T, n) donne au channel de la place pour n valeurs. Les envois réussissent sans récepteur tant que le buffer n'est pas plein ; les réceptions réussissent tant qu'il n'est pas vide. Les valeurs sortent dans l'ordre où elles sont entrées.
Un buffer découple l'émetteur du récepteur pour que de courtes rafales ne bloquent pas l'émetteur. Il ne règle pas le cas d'un producteur durablement plus rapide que son consommateur ; il retarde seulement le moment où l'émetteur bloque. Choisissez une taille de buffer pour une raison précise (le nombre d'émetteurs, une taille de lot connue), pas pour faire disparaître un deadlock.
len(ch) est un instantané. Le temps que vous agissiez, une autre goroutine a pu le modifier, donc ne vous en servez pas pour décider si un envoi va bloquer. Utilisez pour cela select avec un cas default.
Close et range
close(ch) indique aux récepteurs qu'aucune autre valeur ne sera envoyée. Après une fermeture :
- les valeurs déjà dans le buffer sont encore livrées,
- ensuite chaque réception renvoie immédiatement la valeur zéro,
v, ok := <-chindiqueok == false,for v := range chse termine.
La première réception après close obtient encore "last" avec ok == true. La deuxième obtient la valeur zéro "" et false.
Les règles qui provoquent des panics :
- envoyer sur un channel fermé provoque un panic
send on closed channel, - fermer un channel déjà fermé provoque un panic,
- fermer un channel
nilprovoque un panic.
C'est donc uniquement le côté émetteur qui ferme, et une seule fois. Avec plusieurs émetteurs, aucun ne sait quand les autres ont fini ; faites attendre tous les émetteurs par une goroutine séparée (avec un sync.WaitGroup) et fermez le channel après le retour de Wait. La fermeture n'est nécessaire que si un récepteur attend la fin. Un channel non fermé que plus personne ne référence est récupéré par le ramasse-miettes comme n'importe quelle autre valeur.
Types directionnels
Une fonction peut déclarer qu'elle ne fait qu'envoyer ou que recevoir sur un channel. Le compilateur rejette alors l'autre opération.
| Type | Signification | Autorisé |
|---|---|---|
chan T | bidirectionnel | envoi, réception, fermeture |
chan<- T | envoi seul | envoi, fermeture |
<-chan T | réception seule | réception |
Un chan T se convertit implicitement vers l'un ou l'autre type restreint quand vous le passez à une fonction. C'est pour cela que produce ci-dessus peut renvoyer un <-chan int : les appelants peuvent faire un range dessus mais ne peuvent ni y envoyer ni le fermer. Utilisez les types directionnels sur chaque paramètre de fonction où ils s'appliquent. Ils documentent la propriété du channel et transforment une mauvaise utilisation en erreur de compilation.
Deadlock
Si toutes les goroutines sont bloquées et que rien ne peut en réveiller aucune, le runtime interrompt le programme :
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
Le dump des goroutines vous indique quelle opération est bloquée (chan send ici, ou chan receive, sync.WaitGroup.Wait, select). Les causes habituelles :
- un envoi sur un channel sans buffer sans récepteur en cours d'exécution,
- un
rangesur un channel qui n'est jamais fermé, - un compteur de
WaitGroupqui n'atteint jamais zéro, - deux goroutines qui s'attendent mutuellement.
Le runtime ne détecte que le cas où toutes les goroutines sont bloquées. Dans un serveur où d'autres goroutines restent actives (un listener HTTP, un ticker), le même bug ne fait rien planter ; la goroutine bloquée fuit simplement.
Channels nil
Les envois et les réceptions sur un channel nil bloquent indéfiniment. Cela semble inutile, mais c'est une astuce classique dans un select : mettre une variable channel à nil désactive son cas. Ce code fusionne deux channels et cesse d'écouter chacun dès qu'il est fermé :
Sans les affectations à nil, un channel fermé est toujours prêt, et la boucle tournerait à vide sur des valeurs zéro.
Un pipeline
Les channels se composent en pipelines : chaque étape est une goroutine qui reçoit d'un channel et envoie au suivant, et qui ferme sa sortie quand son entrée est terminée.
Chaque étape s'exécute en concurrence, et l'ordre de sortie est déterministe (1, 16, 81) parce que chaque étape est une seule goroutine qui préserve l'ordre. Les fermetures se propagent en cascade : generate ferme, ce qui termine le range de square, qui ferme sa sortie, et ainsi de suite jusqu'à main.
Le point faible de ce pipeline : si main arrêtait de lire plus tôt, les étapes resteraient bloquées sur leurs envois pour toujours. Les vrais pipelines prennent un context.Context ou un channel done et font un select dessus à côté de chaque envoi.
Channel ou mutex
Les channels servent à transférer la propriété de données et à signaler des événements. Un sync.Mutex est plus simple pour protéger un état partagé que de nombreuses goroutines lisent et modifient sur place, comme un cache ou un compteur. Une struct contenant un mutex est souvent plus claire qu'une goroutine qui possède l'état et répond à des requêtes via des channels. Prenez ce qui raccourcit le code et rend la propriété évidente.
Référence rapide
| Opération | channel nil | channel ouvert | channel fermé |
|---|---|---|---|
ch <- v | bloque indéfiniment | bloque jusqu'à la réception ou jusqu'à ce que le buffer ait de la place | panic |
<-ch | bloque indéfiniment | bloque jusqu'à ce qu'une valeur soit disponible | valeurs du buffer, puis valeur zéro |
v, ok := <-ch | bloque indéfiniment | ok vaut true | ok vaut false une fois vidé |
close(ch) | panic | ferme | panic |
len(ch), cap(ch) | 0, 0 | valeurs dans le buffer, taille du buffer | valeurs restantes, taille du buffer |
Questions fréquentes
Quelle est la différence entre un channel avec buffer et un channel sans buffer en Go ?
Un channel sans buffer (make(chan int)) n'a aucun stockage : un envoi bloque jusqu'à ce qu'une autre goroutine reçoive, donc chaque envoi est aussi une remise de main et un point de synchronisation. Un channel avec buffer (make(chan int, 3)) contient jusqu'à 3 valeurs ; les envois ne bloquent que lorsque le buffer est plein et les réceptions que lorsqu'il est vide.
Que se passe-t-il quand on lit un channel fermé en Go ?
Les réceptions sur un channel fermé ne bloquent jamais. Elles vident d'abord les valeurs encore présentes dans le buffer, puis renvoient indéfiniment la valeur zéro du type des éléments. Utilisez v, ok := <-ch pour faire la différence : ok vaut false une fois le channel fermé et vide. Une boucle for v := range ch s'arrête à ce moment-là.
Qui doit fermer un channel en Go ?
L'émetteur, et seulement quand les récepteurs ont besoin de savoir qu'aucune autre valeur n'arrivera (par exemple pour terminer une boucle range). Envoyer sur un channel fermé provoque un panic, tout comme fermer un channel deux fois, donc un récepteur qui ferme le channel entre en concurrence avec les émetteurs. Vous n'avez pas besoin de fermer un channel pour le libérer ; le ramasse-miettes récupère de toute façon les channels inaccessibles.
Que signifie « fatal error: all goroutines are asleep » ?
Toutes les goroutines du programme sont bloquées sur une opération de channel ou un verrou que rien ne pourra jamais débloquer, donc le runtime arrête le programme. La cause la plus fréquente est un envoi sur un channel sans buffer dans main sans autre goroutine en réception, ou un range sur un channel qui n'est jamais fermé.