Cours 4 — Paquetage, premier conteneur Docker, tests unitaires et goroutines
Paquetage et modules
Un module est une collection de paquetages qui sont gérés, versionnés et distribués ensemble. Ils peuvent être téléchargés directement d'un serveur de version (GitHub) ou de serveurs mandataires.
Quand utiliser plusieurs paquetages
Dès qu'un projet réseau grossit, il est courant de séparer le code par responsabilité — par exemple un paquetage client/ et un paquetage server/ dans un même module, ou un paquetage protocole/ qui définit le format des messages partagé par les deux.
Un exemple réel de ce découpage : le module golang.org/x/net, dont le dépôt GitHub (https://github.com/golang/net) et la page pkg.go.dev (https://pkg.go.dev/golang.org/x/net) montrent des dizaines de paquetages regroupés dans un seul module — html/, http2/, websocket/, ipv4/, ipv6/, proxy/, etc. — chacun dans son propre sous-répertoire, chacun important indépendamment (ex. golang.org/x/net/html comme vu plus haut). Ils sont regroupés parce qu'ils touchent tous au réseau, pas parce qu'ils partagent du code entre eux.
Quand l'éviter
Pour un petit programme d'atelier à un seul fichier, créer plusieurs paquetages ajoute de la friction (imports, visibilité) sans bénéfice réel — restez dans package main tant que le fichier reste gérable.
Un module est identifié par son chemin et déclaré dans le fichier go.mod. Le répertoire racine est le répertoire contenant le fichier go.mod. Un paquetage est une collection de fichiers .go contenue dans un sous-répertoire du module :
/go
/newmodule
go.mod
/newpackage1/
/newpackage2/
Par exemple, le module "golang.org/x/net" contient un paquetage dans le répertoire "html". Pour l'inclure, il faut utiliser "golang.org/x/net/html".
Deux librairies réelles qui importent chacune un paquetage de golang.org/x/net, pour voir à quoi ça sert concrètement en dehors d'un exemple d'atelier :
goqueryimportegolang.org/x/net/htmlpour analyser du HTML et le parcourir avec une syntaxe inspirée de jQuery — pratique pour du web scraping.zeroconfimportegolang.org/x/net/ipv4(etipv6) pour envoyer et recevoir des messages multidiffusion (multicast) — la base du protocole mDNS/Bonjour, qui permet à des appareils de se découvrir automatiquement sur un réseau local.
Paquetage local (à l'intérieur du même module)
Le cas le plus simple : un paquetage placé dans un sous-répertoire du module courant s'importe simplement par son chemin, à partir du chemin du module — aucune ligne particulière n'est nécessaire dans le go.mod, contrairement aux cas en ligne vus plus loin :
/local
go.mod → module local
local.go → package main
/crlj
crlj.go → package crlj
package main
import (
"fmt"
c "local/crlj" // avec alias : on appelle ensuite c.Patati()
)
func main() {
fmt.Println(c.Patati())
}
Sans alias, l'import s'écrit simplement "local/crlj" et l'appel devient crlj.Patati() (le nom utilisé par défaut est celui du paquetage — crlj — pas le nom du dossier, bien qu'ils soient identiques ici).
Télécharger un paquetage en ligne
go get rsc.io/quote
Il sera téléchargé dans votre répertoire personnel go, et la version résolue est fixée dans go.mod/go.sum (garantie que tout le monde compile avec exactement le même code source). Depuis Go 1.16, mettre seulement l'import sans faire go get ne suffit pas : go build/go run refuse de télécharger silencieusement une dépendance absente de go.mod et affiche une erreur du genre no required module provides package ...; to add it: go get .... Il faut explicitement lancer go get (ou go mod tidy) au moins une fois. Par contre, l'autocomplétion ne fonctionnera pas tant qu'il ne sera pas téléchargé.
Pour retirer les dépendances non utilisées:
go mod tidy
Créer et publier un module en ligne
- Créez un module local avec un nom complet:
go mod init github.com/drynish/chocolate
- Poussez-le sur un gestionnaire de version (github, gitlab, ...), puis créez un tag > 1 (sinon il est considéré comme bêta) :
git tag v1.0.0
- Le module devient consultable à
https://pkg.go.dev/github.com/<user>/<module>(peut prendre quelques heures à apparaître).
chocolate est réellement publié : dépôt github.com/drynish/chocolate (code source complet consultable directement sur GitHub), aussi indexé sur pkg.go.dev/github.com/drynish/chocolate.
L'appeler depuis un autre module ressemble à n'importe quel import externe — go.mod référence la version publiée, et le code l'importe par son chemin GitHub :
// go.mod
require github.com/drynish/chocolate v1.1.0
// main.go
package main
import "github.com/drynish/chocolate"
func main() {
println(chocolate.AimesTu())
}
Exemple complet (callChocolate) — ressemble volontairement à callDice de la section précédente : consommer un module publié se fait toujours de la même façon (import + appel), que ce soit le vôtre ou celui de quelqu'un d'autre. Ce qui change entre les deux sections, c'est ce qu'on a fait avant — publier soi-même le module plutôt que simplement utiliser un module déjà publié par quelqu'un d'autre.
Pour comparer avec un vrai module déjà publié plutôt qu'avec l'exemple chocolate ci-dessus (qui reste local à ce dépôt de cours) : github.com/google/uuid (dépôt : https://github.com/google/uuid, publié sur https://pkg.go.dev/github.com/google/uuid) est un petit module réel, à un seul paquetage, versionné par tags git — une bonne taille pour comparer sa structure (fichier go.mod à la racine, fichiers .go, fichiers de tests, tags de version) avec celle du module que vous venez de créer.
Pointer temporairement vers un module externe non publié (replace)
Votre module dépend d'un autre module — par exemple un paquetage protocole/ partagé entre un client et un serveur qui sont chacun leur propre module, ou une nouvelle version de chocolate sur laquelle vous travaillez — mais la version dont vous avez besoin n'est pas encore publiée en ligne. La directive replace du go.mod permet de faire pointer temporairement cette dépendance vers une copie locale sur disque, le temps du développement, plutôt que d'essayer de télécharger en ligne une version qui n'existe pas encore :
require github.com/drynish/chocolate v1.1.0
replace github.com/drynish/chocolate => ./chocolate-local
Le chemin après => est un chemin relatif ordinaire — il n'a pas besoin de reproduire le chemin d'import github.com/drynish/chocolate. La version v1.1.0 dans le require est celle réellement publiée en ligne (voir la section précédente) ; tant que le replace est actif, Go l'ignore complètement et utilise plutôt le code du dossier local.
Exemple : le dossier chocolate-local/ y ajoute trois fonctions (Sortes(), PourcentageCacao(), EstVegane()) absentes de la version v1.1.0 publiée en ligne. main.go les appelle ; en commentant la ligne replace et en relançant go build, la compilation échoue puisque la version stable en ligne ne les connaît pas encore — la preuve concrète de ce que replace apporte.
Une fois le module réellement publié sur GitHub avec un tag de version (voir « Créer et publier un module en ligne » plus haut), on retire ces deux lignes et go mod tidy télécharge la vraie version en ligne à sa place.
Premier conteneur Docker
On vient de voir comment empaqueter du code Go pour qu'un autre développeur puisse l'importer (module, go get). Il existe un deuxième niveau de packaging, complémentaire : empaqueter le programme compilé pour qu'il tourne de façon identique sur n'importe quelle machine, sans même que Go y soit installé. C'est ce que fait Docker — un conteneur regroupe votre binaire et tout ce dont il a besoin pour s'exécuter dans une seule image portable.
Un Dockerfile minimal pour un programme Go en ligne de commande :
FROM golang:1.27
WORKDIR /app
COPY go.mod ./
COPY . .
RUN go build -o programme .
CMD ["./programme"]
docker build -t mon-programme:1.0 .
docker run mon-programme:1.0
docker build lit le Dockerfile et produit une image (un modèle en lecture seule) ; docker run en démarre une instance en cours d'exécution, appelée conteneur.
Quand l'utiliser
Dès que vous voulez qu'une autre personne (ou vous-même, plus tard, sur une autre machine) puisse exécuter votre programme sans installer Go ni régler de versions — utile même pour un simple outil de ligne de commande d'atelier, pas seulement pour un serveur.
Quand l'éviter
Pas encore pour un vrai service réseau avec dépendances externes (base de données, plusieurs conteneurs qui doivent communiquer) — cette partie plus riche de Docker (images multi-étapes, images minimales, docker-compose, réseaux Docker) demande un vrai serveur web à empaqueter pour être pertinente. Ici, l'objectif est seulement de prendre l'habitude de dockeriser ce qu'on écrit dès le départ.
Essayer un vrai projet Go dockerisé : traefik/whoami
Pour voir un Dockerfile réel (plutôt que l'exemple minimal ci-dessus) appliqué à un petit serveur réseau, traefik/whoami (https://github.com/traefik/whoami) est un bon candidat : un serveur HTTP en Go, à un seul binaire, qui répond avec le nom d'hôte, les adresses IP et les détails de la requête reçue — pratique pour tester rapidement un réseau ou un reverse proxy, et déjà relié au reste du cours (une route /api en JSON, une route /echo en WebSocket).
Le but ici est de lancer un vrai service réseau dockerisé sans installer Go ni ses dépendances — pas d'obtenir une boucle de développement avec rechargement à chaud. Après une modification du code source, il faut refaire docker build puis relancer le conteneur ; le cache de layers Docker rend ça rapide, mais ce n'est pas instantané comme le serait go run . en local.
Pour le bâtir et le lancer localement à partir de son propre Dockerfile, sans même installer Go sur votre poste :
git clone https://github.com/traefik/whoami.git
cd whoami
docker build -t whoami .
docker run -d -p 8080:80 --name whoami whoami
Puis, dans un navigateur ou avec curl :
curl http://localhost:8080/
curl http://localhost:8080/api
La première commande renvoie du texte brut (nom d'hôte, IP, en-têtes HTTP reçus) ; la seconde, la même information en JSON.
Un coup d'œil au Dockerfile du projet montre déjà, sans qu'il faille encore le comprendre en détail, une technique plus avancée que le Dockerfile minimal ci-dessus : deux étapes de construction — compilation dans une image golang:1-alpine, puis copie du seul binaire compilé dans une image scratch (complètement vide) — pour obtenir une image finale minuscule.
Pour arrêter et retirer le conteneur ensuite :
docker stop whoami
docker rm whoami
Tests unitaires
Go inclut une fonctionnalité simple qui nous permet de faire des tests, par exemple pour une fonction qui convertit une chaîne en nombre puis le double (Nombre1) :
expected := []int{627, 1882, 941, 2824, 1412, 706, 353, 1060, 530, 265, 796, 398, 199, 598, 299, 898, 449, 1348, 674, 337, 1012, 506, 253, 760, 380, 190, 95, 286, 143, 430, 215, 646, 323, 970, 485, 1456, 728, 364, 182, 91, 274, 137, 412, 206, 103, 310, 155, 466, 233, 700, 350, 175, 526, 263, 790, 395, 1186, 593, 1780, 890, 445, 1336, 668, 334, 167, 502, 251, 754, 377, 1132, 566, 283, 850, 425, 1276, 638, 319, 958, 479, 1438, 719, 2158, 1079, 3238, 1619, 4858, 2429, 7288, 3644, 1822, 911, 2734, 1367, 4102, 2051, 6154, 3077, 9232, 4616, 2308, 1154, 577, 1732, 866, 433, 1300, 650, 325, 976, 488, 244, 122, 61, 184, 92, 46, 23, 70, 35, 106, 53, 160, 80, 40, 20, 10, 5, 16, 8, 4, 2, 1}
output := Nombre4(1254)
if !reflect.DeepEqual(output, expected) {
t.Errorf("Output %v not equal to expected %v", output, expected)
}
On peut exécuter les tests avec la commande go test.
Notez que le nommage est important, Go ne va tester que les fichiers qui terminent par "_test.go" et exécuter seulement les fonctions qui débutent par "Test". La majuscule est importante.
Il peut être logique de tester plusieurs possibilités avec un ensemble de valeurs (table-driven tests) :
type Nombre1Test struct {
arg string
expected float64
}
var Nombre1Tests = []Nombre1Test{
{"2.5", 5.0},
{"4.5", 9.0},
{"0", 0.0},
{"-3.5", -7.0},
}
for _, test := range Nombre1Tests {
if output := Nombre1(test.arg); output != test.expected {
t.Errorf("Output %f not equal to expected %f", output, test.expected)
}
}
Quand l'utiliser
Dès qu'une fonction a un comportement qu'on veut garantir sans le revérifier manuellement à chaque changement — typiquement une fonction qui décode un message reçu sur le réseau (ex.: extraire host/port d'une adresse, décoder un en-tête TLV). Les tests table-driven sont particulièrement adaptés ici : chaque ligne du tableau représente un message différent à décoder correctement.
Piège classique : comparer une struct contenant un []byte avec ==. Go refuse carrément de compiler cette comparaison — l'erreur est explicite à la compilation, mais elle surprend souvent la première fois qu'elle survient dans un test :
type Message struct {
Type byte
Charge []byte
}
func TestDecoder(t *testing.T) {
attendu := Message{Type: 1, Charge: []byte("ok")}
resultat := Decoder([]byte{1, 'o', 'k'})
if resultat != attendu { // BUG : ne compile pas — "invalid operation: struct
// containing []byte cannot be compared"
t.Errorf("résultat inattendu")
}
}
Correction : utiliser reflect.DeepEqual pour comparer des structs contenant des slices (ou bytes.Equal pour comparer directement deux []byte) :
if !reflect.DeepEqual(resultat, attendu) {
t.Errorf("résultat inattendu: %v, attendu: %v", resultat, attendu)
}
Exemple complet — un fichier _test.go séparé et exécutable avec go test, sur un domaine différent (extraction des mots uniques d'un texte) : il montre qu'un []string nu, pas seulement une struct qui en contient un, ne peut pas non plus être comparé avec ==, et que reflect.DeepEqual règle les deux cas de la même façon.
Goroutines
La majorité des gros programmes sont en réalité composés de multiples petits programmes, chacun effectuant sa tâche. C'est ainsi qu'un serveur web s'acquitte de sa tâche, chaque requête web étant un petit programme en soi.
Il serait idéal pour un programme de pouvoir exécuter ses tâches toutes en même temps (donc pour un serveur web, de pouvoir répondre à chaque requête instantanément et immédiatement). Rouler plusieurs processus en simultané se nomme concurrence, et Go a de nombreux outils pour y parvenir : les goroutines, vues dans ce cours, et les channels — un mécanisme permettant à des goroutines de s'échanger des données en toute sécurité.
Une goroutine est une fonction capable d'être exécutée en parallèle à d'autres fonctions. Pour créer une goroutine il suffit d'utiliser le mot-clé go avant l'invocation de la fonction.
Notons les différences suivantes avec les notions déjà vues en système d'exploitation:
-
Le programme est un fichier contenant les instructions pour définir un processus.
-
Processus: Un processus est un environnement d'exécution contenant des instructions, des données utilisateurs et des données systèmes. Il contient les informations d'un programme qui s'exécute.
-
Fils d'exécution: Les fils d'exécution sont plus petits qu'un processus ou qu'un programme. Les fils sont créés par les processus et ont chacun leur propre séquence de contrôle et leur pile.
-
Une goroutine est une entité minimale en go qui peut être exécutée de façon concurrente.
Go Scheduler
Le noyau d'un système d'exploitation est nécessaire pour l'exécution des fils d'exécution d'un programme, il faut également un planificateur (scheduler) pour déterminer le fil à exécuter. Suivant ce principe, Go a aussi un planificateur pour l'exécution des goroutines utilisant une technique connue sous le nom m:n scheduling où m goroutines sont exécutées en utilisant n fils d'exécution provenant du système d'exploitation en utilisant le multiplexage.
Concurrence vs Parallélisme
La concurrence et le parallélisme sont deux concepts souvent utilisés en informatique pour gérer l'exécution de plusieurs tâches, mais ils ont des différences clés :
Concurrence
Définition : La concurrence consiste à gérer plusieurs tâches en chevauchement, c'est-à-dire que plusieurs tâches peuvent démarrer, s'exécuter et se terminer dans des périodes de temps qui se chevauchent.
Exemple : Imagine que tu prépares un repas. Tu peux couper les légumes pendant que le poulet marine. Les tâches ne sont pas effectuées en même temps, mais elles se chevauchent dans le temps.
Parallélisme
Définition : Le parallélisme consiste à exécuter plusieurs tâches littéralement en même temps. Cela nécessite généralement un matériel capable de traiter plusieurs tâches simultanément, comme un processeur multicoeur.
Exemple : Sur un ordinateur avec plusieurs cœurs de processeur, chaque coeur peut exécuter une tâche différente en même temps.
En résumé, la concurrence donne l'illusion de la simultanéité en gérant les tâches de manière à ce qu'elles se chevauchent, tandis que le parallélisme implique une exécution simultanée réelle des tâches
Différence entre goroutine et coroutine
Qu'est-ce qu'une coroutine?
Une coroutine est une fonction qui peut être mise en pause à un point précis puis reprise plus tard exactement là où elle s'était arrêtée, en conservant l'état de ses variables locales et sa position d'exécution entre les deux — contrairement à une fonction normale qui s'exécute d'un bout à l'autre en un seul souffle et perd tout son contexte dès qu'elle retourne.
Le point clé : c'est coopératif, pas préemptif. La coroutine elle-même décide quand elle rend la main (via un mot-clé explicite comme yield ou await) — rien ne l'interrompt de force. Go n'a pas de coroutines natives ; l'exemple ci-dessous est en Python, où elles existent via les générateurs :
def compteur():
i = 0
while True:
yield i # pause ici, retourne i, et se souvient d'où on était
i += 1
c = compteur()
print(next(c)) # 0
print(next(c)) # 1 — reprend juste après le yield, i valait déjà 1
print(next(c)) # 2
Chaque appel à next() reprend l'exécution exactement après le yield précédent — la fonction n'a jamais « redémarré », elle était juste en pause. async/await (Python, JS, C#) est la même idée appliquée à l'attente d'opérations asynchrones.
Une coroutine classique tourne généralement sur un seul fil et ne progresse que quand on l'appelle explicitement : elle ne donne donc aucun parallélisme réel par elle-même, seulement de l'alternance ordonnée. C'est ce qui distingue fondamentalement ce modèle des goroutines, détaillé ci-dessous.
Les goroutines en Go et les coroutines en général partagent des similitudes, mais elles ont aussi des différences importantes :
Similitudes
- Légèreté : Les deux sont des unités légères de traitement. Les goroutines sont des threads légers gérés par le runtime de Go, tandis que les coroutines sont des routines qui peuvent être suspendues et reprises.
- Concurrence : Les deux permettent la programmation concurrente. Elles facilitent l'exécution de plusieurs tâches sans bloquer le programme principal.
Différences
Gestion
- Goroutines : Gérées par le runtime de Go, elles sont planifiées sur plusieurs threads système pour atteindre un traitement parallèle. Elles sont créées en utilisant le mot-clé go.
- Coroutines : Généralement gérées par le programmeur. Elles nécessitent des points explicites de suspension et de reprise, souvent à l'aide de mots-clés spécifiques comme yield ou await.
Communication :
- Goroutines : Utilisent des channels pour communiquer entre elles de manière sécurisée et efficace.
- Coroutines : Peuvent utiliser divers mécanismes de communication, y compris des canaux, des files d'attente ou des variables partagées
package main
import "fmt"
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main() {
go f(0)
var input string
fmt.Scanln(&input)
}
Même si ceci n'est pas tellement apparent, ce programme exécute en réalité deux goroutines:
- La fonction main qui est une goroutine implicite et que tous les programmes ont.
- La seconde est créée lorsque l'appel à go f(0) est réalisé.
Normalement quand on appelle une fonction externe, notre programme va exécuter toutes les lignes de la fonction appelée et revient ensuite à la ligne suivant l'appel. Avec une goroutine par contre, on retourne immédiatement à la ligne d'après et nous n'attendons pas l'exécution de la fonction.
C'est pour cette raison qu'il y a un Scanln à la fin, afin de nous assurer que les fonctions appelées aient le temps de s'exécuter avant que le main "exit". Les goroutines sont légères et on peut facilement en créer des centaines. On peut aisément modifier notre programme pour en créer 10 très facilement:
func main() {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
Que se passe-t-il lorsque vous exécutez ce programme? Voici un exemple de la sortie sur mon ordinateur:
1 : 0
1 : 1
1 : 2
...
5 : 0
0 : 0
0 : 1
...
6 : 0
6 : 1
...
Vous ne pouvez ni contrôler ni faire d'hypothèse sur l'ordre d'exécution. La preuve, vous pourriez relancer l'application plusieurs fois et ne pas obtenir le même résultat. Pourquoi?
Ceci dépend du planificateur du système d'exploitation, du planificateur de GO et de la charge de votre système d'exploitation.
Exemple — deux goroutines qui impriment chacune 200 fois leur propre lettre (a ou b), pour observer visuellement l'entrelacement plutôt qu'une simple alternance.
Vous remarquez que l'exécution est plutôt synchrone, dans le sens où lorsqu'un processus a du temps d'exécution, il exécute toute sa tâche. Ajoutons du délai afin de calmer l'exécution avec time.Sleep et rand.Intn:
package main
import (
"fmt"
"time"
"math/rand"
)
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
amt := time.Duration(rand.Intn(250))
time.Sleep(time.Millisecond * amt)
}
}
f affiche les nombres de 0 à 9 et attend entre 0 et 250 ms après chaque exécution. Les goroutines sont donc en mesure de s'exécuter de façon simultanée.
Race condition sur une variable partagée
Les exemples précédents montrent que l'ordre d'affichage de plusieurs goroutines est imprévisible. Le même genre d'imprévisibilité touche aussi les données : quand plusieurs goroutines lisent et modifient la même variable sans se coordonner, le résultat final peut être incorrect, et varier d'une exécution à l'autre.
Pour cette démonstration, plutôt qu'attendre une entrée clavier avec fmt.Scanln comme dans les exemples précédents, on utilise sync.WaitGroup pour attendre que toutes les goroutines terminent : Add(1) annonce une tâche de plus à attendre, Done() signale qu'une tâche est terminée, et Wait() bloque jusqu'à ce que toutes les tâches annoncées soient terminées.
compteur := 0
var attente sync.WaitGroup
for i := 0; i < 1000; i++ {
attente.Add(1)
go func() {
defer attente.Done()
compteur++ // BUG : trois étapes non atomiques — lire, additionner, réécrire
}()
}
attente.Wait()
fmt.Println(compteur) // souvent < 1000, et différent à chaque exécution
(Depuis Go 1.25, attente.Go(fonction) combine Add(1) et le go func() { defer attente.Done(); fonction() }() ci-dessus en un seul appel — la forme explicite reste utile à connaître puisque Go fait essentiellement la même chose en coulisses, à l'exception du traitement d'une panique dans fonction.)
compteur++ n'est pas une seule opération : c'est lire la valeur actuelle, y additionner 1, puis réécrire le résultat. Si deux goroutines lisent la même valeur avant que l'une des deux ait eu le temps de réécrire, une incrémentation est perdue. C'est une race condition : un accès concurrent non protégé à une variable partagée. Le détecteur intégré à Go (go run -race .) confirme le problème même dans une exécution où le résultat affiché serait, par chance, correct.
Quand l'utiliser
Dès qu'une tâche peut progresser indépendamment des autres sans bloquer le reste du programme. Le cas d'usage central en programmation réseau : lancer une goroutine par connexion cliente acceptée, pour que le traitement d'un client lent ne bloque pas l'acceptation des suivants.
func main() {
listener, _ := net.Listen("tcp", ":9000")
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go traiterClient(conn) // une goroutine par connexion ; le listener continue d'accepter
}
}
Une goroutine occupe environ 2 Ko de pile au départ (extensible au besoin), contre plusieurs Mo pour un fil d'exécution du système d'exploitation — un serveur peut donc raisonnablement garder des dizaines de milliers de connexions ouvertes simultanément, une goroutine chacune, là où le même nombre de fils OS épuiserait la mémoire disponible.
Quand l'éviter
Si le nombre de tâches simultanées n'est pas borné et que chacune consomme une ressource limitée (une connexion à une base de données, un descripteur de fichier) — lancer une goroutine sans limite sous forte charge peut épuiser cette ressource même si la mémoire, elle, tiendrait le coup. Une solution existe pour limiter le nombre de goroutines actives à la fois (un « pool de workers »), en bornant leur nombre plutôt que d'en lancer une par tâche.
Piège fréquent en programmation réseau : fuite de goroutine. Une goroutine bloquée pour toujours (sur un channel qui ne recevra jamais rien, ou une lecture réseau qui n'aboutira jamais parce que personne ne ferme la connexion) n'est jamais récupérée par le ramasse-miettes — le programme accumule des goroutines fantômes jusqu'à épuiser la mémoire.
func gererClient(conn net.Conn, notifications chan string) {
go func() {
for msg := range notifications { // BUG : si "notifications" n'est jamais fermé ni
fmt.Fprintln(conn, msg) // alimenté après la fin de gererClient, cette
} // goroutine reste bloquée pour toujours ici
}()
// ... traitement de conn, qui peut se terminer (et fermer conn) sans jamais fermer "notifications" ...
}
On peut confirmer une fuite de goroutines en développement avec runtime.NumGoroutine() : un nombre qui augmente sans jamais redescendre, même après la fermeture des connexions clientes, en est le signe.