Primitives de canal en Golang

GolangGolangBeginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

L'une des caractéristiques les plus attrayantes du langage Go est son support natif pour la programmation concurrente. Il offre un soutien puissant pour le parallélisme, la programmation concurrente et la communication réseau, permettant une utilisation plus efficace des processeurs multi-cœurs.

Contrairement à d'autres langages qui réalisent la concurrence via la mémoire partagée, Go atteint la concurrence en utilisant des canaux (channels) pour la communication entre différents goroutines.

Les goroutines sont les unités de concurrence dans Go et peuvent être considérées comme des threads légers. Elles consomment moins de ressources lors des changements de contexte par rapport aux threads traditionnels.

Les canaux (channels) et les goroutines constituent ensemble les primitives de concurrence dans Go. Dans cette section, nous allons apprendre à connaître les canaux (channels), cette nouvelle structure de données.

Points clés de connaissance :

  • Types de canaux (channels)
  • Création de canaux (channels)
  • Opérations sur les canaux (channels)
  • Blocage des canaux (channels)
  • Canaux unidirectionnels (Unidirectional channels)

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"]) go(("Golang")) -.-> go/ConcurrencyGroup(["Concurrency"]) go/BasicsGroup -.-> go/values("Values") go/BasicsGroup -.-> go/variables("Variables") go/DataTypesandStructuresGroup -.-> go/structs("Structs") go/ConcurrencyGroup -.-> go/goroutines("Goroutines") go/ConcurrencyGroup -.-> go/channels("Channels") go/ConcurrencyGroup -.-> go/select("Select") subgraph Lab Skills go/values -.-> lab-149096{{"Primitives de canal en Golang"}} go/variables -.-> lab-149096{{"Primitives de canal en Golang"}} go/structs -.-> lab-149096{{"Primitives de canal en Golang"}} go/goroutines -.-> lab-149096{{"Primitives de canal en Golang"}} go/channels -.-> lab-149096{{"Primitives de canal en Golang"}} go/select -.-> lab-149096{{"Primitives de canal en Golang"}} end

Aperçu des canaux (Channels)

Les canaux (channels) sont une structure de données spéciale principalement utilisée pour la communication entre les goroutines.

Contrairement à d'autres modèles de concurrence qui utilisent des mutex et des fonctions atomiques pour accéder en toute sécurité aux ressources, les canaux (channels) permettent la synchronisation en envoyant et en recevant des données entre plusieurs goroutines.

En Go, un canal (channel) est comme une bande transporteuse ou une file d'attente qui suit la règle du Premier Entré, Premier Sorti (First-In-First-Out - FIFO).

Types et déclaration de canaux (Channels)

Un canal (channel) est un type référence, et son format de déclaration est le suivant :

var channelName chan elementType

Chaque canal (channel) a un type spécifique et ne peut transporter que des données du même type. Voyons quelques exemples :

Créez un fichier nommé channel.go dans le répertoire ~/project :

cd ~/project
touch channel.go

Par exemple, déclarons un canal (channel) de type int :

var ch chan int

Le code ci-dessus déclare un canal (channel) de type int nommé ch.

En plus de cela, il existe de nombreux autres types de canaux (channels) couramment utilisés.

Dans le fichier channel.go, écrivez le code suivant :

package main

import "fmt"

func main() {
    var ch1 chan int   // Déclare un canal (channel) d'entiers
    var ch2 chan bool  // Déclare un canal (channel) de booléens
    var ch3 chan []int // Déclare un canal (channel) qui transporte des tableaux d'entiers
    fmt.Println(ch1, ch2, ch3)
}

Le code ci-dessus déclare simplement trois types différents de canaux (channels).

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

<nil> <nil> <nil>

Initialisation des canaux (Channels)

À l'instar des maps, les canaux (channels) doivent être initialisés après leur déclaration avant de pouvoir être utilisés.

La syntaxe pour initialiser un canal (channel) est la suivante :

make(chan elementType)

Dans le fichier channel.go, écrivez le code suivant :

package main

import "fmt"

func main() {
    // Canal (channel) qui stocke des données entières
    ch1 := make(chan int)

    // Canal (channel) qui stocke des données booléennes
    ch2 := make(chan bool)

    // Canal (channel) qui stocke des données de type []int
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est présentée ci-dessous :

0xc0000b4000 0xc0000b4040 0xc0000b4080

Dans le code ci-dessus, trois types différents de canaux (channels) sont définis et initialisés.

En examinant la sortie, nous pouvons constater que, contrairement aux maps, l'impression directe du canal (channel) ne révèle pas son contenu.

L'impression directe d'un canal (channel) ne donne que l'adresse mémoire du canal lui-même. En effet, comme un pointeur, un canal (channel) n'est qu'une référence à une adresse mémoire.

Alors, comment accéder et manipuler les valeurs dans un canal (channel)?

Opérations sur les canaux (Channels)

Envoi de données

Les opérations sur les canaux (channels) ont leurs propres méthodes uniques. En général, lorsque nous travaillons avec d'autres types de données, nous utilisons = et des index pour accéder et manipuler les données.

Mais pour les canaux (channels), nous utilisons <- pour les opérations d'envoi et de réception.

Dans un canal (channel), écrire des données s'appelle envoyer, ce qui signifie envoyer une donnée dans le canal.

Alors, comment envoyer des données?

Dans le fichier channel.go, écrivez le code suivant :

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    fmt.Println(ch)
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

0xc0000b4000

Bien que nous ayons mis des données dans le canal (channel), nous ne pouvons pas les voir directement à partir de la sortie de la valeur du canal.

C'est parce que les données dans le canal (channel) doivent encore être reçues avant de pouvoir être utilisées.

Réception de données

Qu'est-ce que recevoir depuis un canal (channel) signifie? Lorsque nous apprenons les maps ou les slices, nous pouvons extraire n'importe quelle donnée de ces types de données en fonction d'indices ou de clés.

Dans le cas des canaux (channels), nous ne pouvons extraire que la valeur la plus ancienne qui est entrée dans le canal.

Après que la valeur a été reçue, elle n'existera plus dans le canal (channel).

En d'autres termes, recevoir depuis un canal (channel) est comme interroger et supprimer des données dans d'autres types de données.

Dans le fichier channel.go, écrivez le code suivant :

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

10
20

Nous pouvons voir que la valeur 10, qui est entrée en premier dans le canal (channel), est affichée en premier, et après avoir été affichée, elle n'existe plus dans le canal.

La valeur 20, qui est entrée plus tard, sera affichée plus tard, mettant en évidence le principe du premier entré, premier sorti (First-In-First-Out - FIFO) du canal (channel).

Fermeture du canal (Channel)

Après qu'un canal (channel) n'est plus nécessaire, il doit être fermé. Cependant, lors de la fermeture d'un canal (channel), nous devons faire attention aux points suivants :

  • Envoyer une valeur à un canal (channel) fermé entraînera une erreur à l'exécution.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
    
        fmt.Println(<-ch)
    
        close(ch)
        ch <- 20
    }

    La sortie du programme est la suivante :

    10
    panic: send on closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:10 +0x1a0
    exit status 2
  • Recevoir depuis un canal (channel) fermé continuera à extraire des valeurs jusqu'à ce qu'il soit vide.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Exécutez le programme en utilisant la commande suivante :

    go run channel.go

    La sortie du programme est la suivante :

    10
    20
    30
  • Recevoir depuis un canal (channel) fermé et vide recevra la valeur initiale par défaut du type de données correspondant.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Exécutez le programme en utilisant la commande suivante :

    go run channel.go

    La sortie du programme est la suivante :

    10
    20
    30
    0
    0
  • Fermer un canal (channel) déjà fermé déclenchera une erreur.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
        close(ch)
        close(ch)
        fmt.Println(<-ch)
    }

    La sortie du programme est la suivante :

    panic: close of closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:9 +0x75
    exit status 2

Blocage des canaux (Channels)

Les étudiants attentifs auront peut-être remarqué que lorsque nous avons présenté les déclarations et l'initialisation des canaux (channels), nous n'avons pas spécifié la capacité du canal :

package main

import "fmt"

func main() {
    // Stocke des données entières dans le canal (channel)
    ch1 := make(chan int) // Idem ci-dessous

    // Stocke des données booléennes dans le canal (channel)
    ch2 := make(chan bool)

    // Stocke des données de type []int dans le canal (channel)
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Mais lorsque nous avons démontré les opérations sur les canaux (channels), nous avons spécifié la capacité du canal :

package main

import "fmt"

func main() {
    // Spécifie la capacité du canal (channel) à 3
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Pour les canaux (channels) sans capacité spécifiée, on les appelle des canaux non tamponnés (unbuffered channels), qui n'ont pas d'espace tampon.

Si l'émetteur ou le récepteur n'est pas prêt, la première opération sera bloquée jusqu'à ce que l'autre opération soit prête.

chan1 := make(chan int) // Canal non tamponné (unbuffered channel) de type int

Pour les canaux (channels) avec une capacité et un espace tampon spécifiés, on les appelle des canaux tamponnés (buffered channels).

chan2 := make(chan int, 5) // Canal tamponné (buffered channel) de type int avec une capacité de 5

Ils fonctionnent comme une file d'attente, en respectant la règle du Premier Entré, Premier Sorti (First In First Out - FIFO).

Pour les canaux tamponnés (buffered channels), nous pouvons utiliser des opérations d'envoi pour ajouter des éléments à la fin de la file d'attente et des opérations de réception pour supprimer des éléments du début de la file d'attente.

Que se passe-t-il si nous mettons plus de données dans un canal tamponné (buffered channel) que sa capacité? Vérifions cela. Écrivez le code suivant dans le fichier channel1.go :

cd ~/project
touch channel1.go
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    ch <- 20
    fmt.Println("succeed")
}

Exécutez le programme en utilisant la commande suivante :

go run channel1.go

La sortie du programme est la suivante :

fatal error: all goroutines are asleep - deadlock!

Nous constatons que le programme est en blocage (deadlock) car le canal (channel) est déjà plein.

Pour résoudre ce problème, nous pouvons extraire d'abord les données du canal (channel). Écrivez le code suivant dans channel.go :

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    fmt.Println("Data extracted:", <-ch)

    ch <- 20
    fmt.Println("succeed")
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

Data extracted: 10
succeed

Nous pouvons utiliser continuellement le canal (channel) avec une capacité limitée en adoptant une stratégie de prendre, utiliser, prendre et utiliser à nouveau.

Canaux unidirectionnels (Unidirectional Channels)

Par défaut, un canal (channel) est bidirectionnel et peut être lu ou écrit. Parfois, nous devons restreindre l'utilisation d'un canal, par exemple, nous voulons nous assurer qu'un canal ne peut être que écrit ou seulement lu à l'intérieur d'une fonction. Comment pouvons-nous y parvenir?

Nous pouvons déclarer explicitement un canal avec une utilisation restreinte avant de l'initialiser.

Canal en écriture seule (Write-only Channel)

Un canal en écriture seule signifie que, à l'intérieur d'une fonction particulière, le canal ne peut être que écrit et non lu. Déclarons et utilisons un canal en écriture seule de type int :

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 10
    fmt.Println("Data written to channel")
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    fmt.Println("Data received:", <-ch)
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

Data written to channel
Data received: 10

Dans cet exemple, la fonction writeOnly restreint le canal à l'écriture seule.

Canal en lecture seule (Read-only Channel)

Un canal en lecture seule signifie que, à l'intérieur d'une fonction particulière, le canal ne peut être que lu et non écrit. Déclarons et utilisons un canal en lecture seule de type int :

package main

import "fmt"

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    ch <- 20
    readOnly(ch)
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

Data received from channel: 20

Dans cet exemple, la fonction readOnly restreint le canal à la lecture seule.

Combinaison de canaux en écriture seule et en lecture seule

Vous pouvez combiner des canaux en écriture seule et en lecture seule pour démontrer comment les données circulent à travers des canaux restreints :

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 30
    fmt.Println("Data written to channel")
}

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    readOnly(ch)
}

Exécutez le programme en utilisant la commande suivante :

go run channel.go

La sortie du programme est la suivante :

Data written to channel
Data received from channel: 30

Cet exemple illustre comment un canal en écriture seule peut transmettre des données à un canal en lecture seule.

Résumé

Dans ce laboratoire (lab), nous avons appris les bases des canaux (channels), qui incluent :

  • Les types de canaux (channels) et leur déclaration
  • Les méthodes d'initialisation des canaux (channels)
  • Les opérations sur les canaux (channels)
  • Le concept de blocage des canaux (channel blocking)
  • La déclaration de canaux unidirectionnels (unidirectional channels)