Goroutines et gestion du temps d'exécution

Le mot‑clé go crée une goroutine, une fonction exécutée de façon concurrente par le runtime Go. Contrairement aux threads du système d’exploitation, les goroutines sont légères ; le texte indique qu’on peut en lancer des centaines voire des milliers sans saturer les ressources. Chaque goroutine possède son propre stack, initialement de quelques kilo‑octets, qui s’étend dynamiquement, ce qui explique cette légèreté. Le programme principal main est lui‑même une goroutine, démarrée implicitement, et son arrêt entraîne la terminaison brutale de toutes les goroutines encore actives.

Pour synchroniser l’achèvement de plusieurs goroutines, le mini‑livre utilise sync.WaitGroup. Le compteur interne du WaitGroup augmente via Add(n) et diminue via Done(). L’appel Wait() bloque la goroutine appelante jusqu’à ce que le compteur revienne à zéro, garantissant ainsi que le processus principal ne se termine pas prématurément. La variante wg.Go(func(){...}) encapsule ces trois étapes : incrémentation, lancement et décrémentation automatique, réduisant le risque d’oubli de Done().

Canaux : synchronisation et communication

Les canaux (chan) offrent un moyen sûr de transmettre des valeurs entre goroutines. Un envoi (ch <- v) bloque la goroutine émettrice jusqu’à ce qu’une goroutine réceptrice effectue v := <-ch. Cette synchronisation implicite élimine les verrous explicites dans de nombreux scénarios. Le texte montre la création d’un canal non tamponné avec make(chan string) et son utilisation pour transmettre le texte "ping", illustrant le blocage réciproque.

Le livre détaille également la fermeture de canal via close(ch). Après la fermeture, les récepteurs continuent de lire les valeurs restantes dans le tampon, puis reçoivent le zéro du type et ok == false. Cette double valeur (v, ok := <-ch) permet de détecter la fin du flux sans lever d’exception. Le guide précise que fermer un canal plusieurs fois ou écrire dans un canal fermé déclenche un panic, soulignant la nécessité d’une discipline d’écriture.

Les canaux directionnels (chan<- et <-chan) renforcent la sécurité de type en interdisant les opérations non permises. Le runtime accepte la conversion implicite d’un canal bidirectionnel vers un canal directionnel lorsqu’il est passé en argument, comme démontré par les fonctions func(in chan<- int) et func(out <-chan int). Les canaux tamponnés (make(chan int, N)) introduisent un buffer de capacité N, permettant aux envois de ne pas bloquer tant que le tampon n’est pas plein. Le texte montre un tampon de taille 3, où trois valeurs sont injectées sans blocage, puis deux sont lues, illustrant le comportement FIFO.

Sélection et modèles avancés

Le mot‑clé select agit comme un switch spécialisé pour les canaux. Il examine les cas prêts : si plusieurs opérations de canal sont non bloquantes, le runtime en choisit une aléatoirement, assurant une distribution équitable. Un cas default permet d’éviter le blocage complet lorsque toutes les opérations sont bloquées, offrant ainsi un point d’entrée pour implémenter des boucles de polling ou des time‑outs.

En combinant select avec des canaux fermés, on peut détecter la fin d’un flux sans recourir à des variables de contrôle externes. Par exemple, un select qui lit v, ok := <-ch dans un cas et qui exécute un default dans l’autre permet de poursuivre le traitement tant que des données sont disponibles, tout en restant réactif aux changements d’état du canal.