Pourquoi Goroutines et channels en Go ?
Contexte réel : pourquoi un dev a besoin de ca au quotidien
Le développement d'applications modernes nécessite souvent la gestion concrète des tâches parallèles et asynchrones pour assurer une performance élevée et une réponse rapide à l'utilisateur. En utilisant les goroutines et les channels en Go, les développeurs peuvent structurer leur code de manière plus efficace et éviter les problèmes liés aux threadings traditionnels.
Un cas d'usage concret en 2-3 phrases
Imaginez un service web qui doit traiter des images uploadées par les utilisateurs. Chaque image peut être redimensionnée, compressée ou convertie en format différent de manière indépendante. En utilisant des goroutines pour chaque tâche et des channels pour communiquer entre elles, le serveur peut gérer plusieurs images simultanément sans surcharger la CPU.
Prerequis
- Connaissances en Go
- Variables, structures de contrôle (if, for), fonctions
- Familiarité avec les concepts de concurrence
- Threadings et synchronisation
- Installation d'Go : https://golang.org/dl/
Concepts fondamentaux
Goroutines
Une goroutine est une tâche légère qui peut être lancée en parallèle. Elle est similaire à un thread mais avec une gestion plus efficace de la mémoire et des ressources.
Schéma mental :
|-----------------|
| Goroutine 1 |
| |
|-----------------|
|
v
|-----------------|
| Goroutine 2 |
| |
|-----------------|
Code fonctionnel :
package main
import (
"fmt"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Channels
Un channel est un moyen de passer des valeurs entre les goroutines. Il permet d'échanger et de synchroniser les données entre plusieurs routines en temps réel.
Schéma mental :
|-----------------|
| Goroutine 1 |
| |
|-----------------|
|
v
|-----------------|
| Channel |
| |
|-----------------|
|
v
|-----------------|
| Goroutine 2 |
| |
|-----------------|
Code fonctionnel :
package main
import (
"fmt"
)
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // envoie la somme au channel
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // récupère les sommes des channels
fmt.Println(x, y, x+y)
}
Mise en pratique : projet fil rouge
Projet : Mini-gestionnaire de tâches
Le mini-gestionnaire de tâches permettra aux utilisateurs d'ajouter, supprimer et afficher leurs tâches. Les ajouts seront traités par une goroutine pour éviter le blocage de la fonction principale.
Étapes
Création du fichier
main.gopackage main import ( "fmt" "sync" ) type Task struct { id int title string done bool } var tasks = []Task{} var mu sync.Mutex func addTask(title string) { mu.Lock() task := Task{id: len(tasks) + 1, title: title, done: false} tasks = append(tasks, task) mu.Unlock() } func removeTask(id int) { mu.Lock() for i, t := range tasks { if t.id == id { tasks = append(tasks[:i], tasks[i+1:]...) break } } mu.Unlock() } func listTasks() []Task { return tasks } func main() { var wg sync.WaitGroup go addTask("Faire les courses") go addTask("Nettoyer la maison") wg.Add(1) go func() { defer wg.Done() time.Sleep(time.Second * 2) removeTask(1) }() wg.Wait() for _, task := range listTasks() { fmt.Printf("ID: %d, Title: %s, Done: %t\n", task.id, task.title, task.done) } }Commande à exécuter
go run main.go
Explications
Task: Structure représentant une tâche.tasks: Slice de tâches partagées entre les goroutines.mu: Mutex pour synchroniser l'accès aux données partagées.addTask: Ajoute une nouvelle tâche à la liste en utilisant une goroutine.removeTask: Supprime une tâche de la liste en utilisant une goroutine.listTasks: Renvoie la liste des tâches.
Erreurs frequentes et debugging
1. Erreur : Goroutine sans channel
Code incorrect :
package main
func main() {
go func() {
fmt.Println("Hello from goroutine")
}()
}
Code correct :
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
2. Erreur : Channel non bufferisé
Code incorrect :
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second)
fmt.Println("Worker", id, "started job", j)
results <- j * 2
}
}
func main() {
jobs := make(chan int)
results := make(chan int)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
<-results
}
}
Code correct :
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second)
fmt.Println("Worker", id, "started job", j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= 5; a++ {
<-results
}
}
3. Erreur : Deadlock
Code incorrect :
package main
import (
"fmt"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Code correct :
package main
import (
"fmt"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Pour aller plus loin
1. Buffering channels
L'apprentissage des channels bufferisés peut aider à gérer les cas où le producteur ne produit pas aussi rapidement que le consommateur.
Liens :
2. Select statements
Les select statements offrent une façon plus flexible de travailler avec plusieurs channels en attendant les valeurs à recevoir.
Liens :
3. Unbuffered channels vs Buffered channels
Il est important d'understandre la différence entre les channels bufferisés et non bufferisés et quand utiliser chaque type.
Liens :
Défi pratique
Défi : Mini-API de blog
Créez une API simple en utilisant les goroutines et les channels pour gérer des posts. Les fonctionnalités devraient inclure l'ajout, la suppression et la récupération de posts.
Instructions :
- Créez un fichier
main.go - Implémentez les endpoints suivants :
POST /postspour ajouter un postGET /postspour récupérer tous les postsDELETE /posts/:idpour supprimer un post
- Utilisez des goroutines pour gérer les requêtes en parallèle.
- Utilisez des channels pour synchroniser l'accès aux données partagées.
Indice :
- Pour simplifier, utilisez un slice pour stocker les posts plutôt qu'une base de données réelle.
- Assurez-vous que chaque endpoint retourne le bon code HTTP et les bons headers.
Ressources supplémentaires :