Attendre plusieurs channels
select ressemble à un switch, mais chaque cas est un envoi ou une réception sur un channel. Il bloque jusqu'à ce qu'un cas puisse avoir lieu, puis exécute celui-là.
Avec ces délais, le premier select obtient le résultat rapide et le second le résultat lent. Aucune réception n'a à attendre l'autre channel. Un simple <-slow suivi de <-fast les traiterait dans un ordre fixe, quel que soit celui arrivé en premier.
Comment select s'évalue :
- Toutes les expressions de channel et les valeurs à envoyer sont évaluées une fois, dans l'ordre du source, au démarrage du
select. - Si un ou plusieurs cas sont prêts, l'un d'eux est choisi au hasard.
- Si aucun n'est prêt et qu'il y a un
default, ledefaults'exécute. - Sinon la goroutine bloque jusqu'à ce qu'un cas soit prêt.
Un select {} vide bloque indéfiniment. On le voit parfois à la fin de main dans des programmes dont le vrai travail se fait dans d'autres goroutines.
Choix aléatoire parmi les cas prêts
Quand plusieurs cas sont prêts en même temps, select ne privilégie pas le premier listé. Ce programme remplit deux channels avec buffer puis fait 1000 select :
La répartition entre countA et countB change à chaque exécution et tombe près de 500 chacun. Le choix aléatoire est délibéré : il empêche un channel très actif d'affamer les autres. Si vous avez besoin d'une priorité, voyez le motif plus bas.
Opérations non bloquantes avec default
Avec un cas default, select ne bloque jamais. Cela transforme un envoi ou une réception en tentative :
Abandonner du travail quand un buffer est plein est la façon de délester la charge ou d'émettre des métriques sans jamais bloquer l'appelant.
Ne mettez pas de default dans un select à l'intérieur d'une boucle for juste pour « vérifier » les channels en boucle. Quand rien n'est prêt, la boucle tourne à vide à 100 % de CPU. Bloquez plutôt, et ajoutez un cas de timeout si vous devez vous réveiller périodiquement.
Timeouts
time.After(d) renvoie un channel qui reçoit une fois après d. Mettez-le en concurrence avec le vrai travail :
Le premier appel renvoie "data". Le second renvoie l'erreur de timeout après 50 ms, bien avant que le worker ait fini. Le channel de résultat a exprès un buffer de 1. Quand le timeout gagne, personne ne reçoit jamais sur result ; avec un channel sans buffer, la goroutine du worker bloquerait indéfiniment sur son envoi et fuirait.
Dans une boucle, time.After crée un nouveau timer à chaque itération, ce qui est exactement ce qu'il faut pour un timeout d'inactivité par message (« aucun message depuis 1 seconde »). Pour une échéance globale sur de nombreuses opérations, créez un seul timer ou un seul context avant la boucle. Depuis Go 1.23, les timers qui ne sont plus référencés sont récupérés par le ramasse-miettes même s'ils ne se sont pas déclenchés, donc time.After dans une boucle ne retient plus la mémoire jusqu'au déclenchement de chaque timer, comme dans les versions plus anciennes (cela demande go 1.23 ou plus dans go.mod).
Boucles for-select et channels d'arrêt
Une goroutine qui tourne jusqu'à ce qu'on lui dise de s'arrêter est une boucle for autour d'un select avec un cas pour le travail et un pour l'arrêt :
Fermer quit plutôt que d'envoyer dessus est l'idiome : une fermeture est vue par tous les récepteurs, maintenant et plus tard, donc un seul close arrête n'importe quel nombre de workers. Le channel done permet à main d'attendre que le worker soit réellement terminé.
Dans du vrai code, le channel d'arrêt est généralement un context.Context : case <-ctx.Done():. Cela fonctionne de la même façon (Done() renvoie un channel fermé lors de l'annulation) et transporte aussi des échéances et la raison de l'arrêt. La page sur le context le couvre.
break dans un select
break dans un cas de select quitte le select, pas le for englobant. C'est une source fréquente de boucles qui ne se terminent jamais. Utilisez return, ou étiquetez la boucle :
Priorité entre channels
Comme select choisit au hasard, vous ne pouvez pas classer les cas dans une même instruction. Pour qu'un channel gagne dès qu'il a quelque chose, testez-le d'abord seul :
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
Le premier select retourne immédiatement si l'annulation a déjà eu lieu. Sans lui, un flux régulier de tâches pourrait continuer à gagner le tirage au sort un moment après l'annulation de ctx.
Les channels nil désactivent un cas
Un envoi ou une réception sur un channel nil n'est jamais prêt, donc un cas sur un channel nil est de fait désactivé. Mettre une entrée fermée à nil est la façon d'arrêter de la surveiller tout en continuant avec les autres ; la page sur les channels montre une boucle de fusion construite ainsi. La même astuce active et désactive un timeout : gardez var timeout <-chan time.Time à nil jusqu'à ce que vous en ayez besoin, puis affectez time.After(d).
Erreurs courantes
- S'attendre à l'ordre du source. Le premier cas listé n'est pas privilégié.
- Une boucle active avec
default. Unfor { select { ... default: } }qui n'a rien à faire consomme un cœur de CPU. - Faire fuir le perdant. Quand un timeout gagne, la goroutine qui aurait envoyé le résultat doit quand même pouvoir terminer. Donnez à son channel un buffer de 1.
- Un
breakqui ne quitte que leselect. Utilisez une étiquette oureturn. - Un
time.Afterpar itération utilisé comme échéance globale. Il redémarre à chaque itération ; créez l'échéance une fois, hors de la boucle.
Questions fréquentes
Que fait select en Go ?
select attend que l'une de ses opérations de channel (un envoi ou une réception) puisse avoir lieu, puis exécute ce cas. Si plusieurs sont prêts en même temps, il en choisit un au hasard. Sans cas default, il bloque jusqu'à ce qu'un cas soit prêt ; avec default, il ne bloque jamais.
Comment ajouter un timeout à une réception sur un channel en Go ?
Mettez la réception et un timer dans un même select : select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Le premier arrivé l'emporte. Dans une boucle, ou quand un appelant a déjà une échéance, utilisez plutôt un context.Context avec context.WithTimeout et faites un select sur ctx.Done().
select choisit-il les cas dans l'ordre en Go ?
Non. Quand plus d'un cas est prêt, Go choisit de façon uniformément aléatoire, donc aucun cas ne peut affamer les autres. Si vous avez besoin d'une priorité, testez d'abord le channel prioritaire dans son propre select avec un default, puis passez à un select sur tous les channels.
Pourquoi break ne sort-il pas de ma boucle for-select ?
Dans un select, break ne quitte que l'instruction select, pas le for qui l'entoure. Utilisez return, ou placez une étiquette sur la boucle (loop: for { select { case <-done: break loop } }).