Publié par
Il y a 3 semaines · 6 minutes · Back, Front

Amitié et Go Channels

Retour d’expérience sur la création d’un outil en ligne de commande concurrent et simple en Golang.

Un ami m’a contacté en me demandant s’il était possible de lui créer un outil qui permettrait de vérifier les pages d’un CMS. Il avait un fichier CSV avec plus de 2000 URL à vérifier régulièrement et ne souhaitait pas le faire à la main.

Ma première réaction a été de lui conseiller d’utiliser Bash ; en effet, une simple boucle et un curl ferait amplement l’affaire. Cependant, j’ai rapidement changé d’avis :

  1. il travaille sur Windows
  2. plus de 2000 HEAD, même en Bash c’est assez long
  3. et pour finir, il n’est pas développeur

L’amitié… et l’envie de coder en Go ont été plus fortes, j’ai donc proposé de lui faire un petit utilitaire.

Cet article ne parle pas des bases de Golang, mais pour les personnes intéressées je vous conseille l’article GO Basics.

Les channels, les goroutines ainsi que la primitive de synchronisation sync.WaitGroup m’ont permis d’implémenter facilement un outil de vérification d’URL concurrent et simple ; cet article vous propose une description de ces technologies et de leurs utilisations. Tout le code est disponible sur github.

Technologies

Goroutines

Les goroutines sont des fonctions qui s’exécutent en concurrence avec d’autres et dans le même espace d’adresse. Elles me permettent, par exemple, de lancer plusieurs requêtes HTTP en concurrence et de ne pas avoir à attendre qu’une requête HTTP se termine pour en lancer une autre.

Channels

Les channels sont des moyens de communication et de synchronisation entre les goroutines. Autrement dit, elles permettent de « transmettre » des données (int, string, struct…) entre des fonctions concurrentes (goroutines). Ici, une fois la requête HTTP effectuée, la réponse est envoyée via un channel à une goroutine chargé d’écrire cette réponse dans un fichier. Le channel ayant un buffer de 10 (par défaut 1), il ne pourra pas y avoir plus de 10 écritures sur le disque en parallèle.

sync.WaitGroup

WaitGroup est une primitive de synchronisation de goroutines proposé dans le package sync de la libraire Golang. Elle propose trois méthodes Add, Done et Wait qui permettent de, respectivement: incrémenter le nombre de goroutine à attendre, décrémenter le nombre de goroutines à attendre et bloquer l’exécution du code jusqu’à ce que le nombre de goroutines à attendre arrive à 0.

Le code

checkUrl

Cette fonction est le « cœur de métier » de l’application. La requête HTTP y est exécuté avec  l’URL reçue en paramètre et nous envoyons la réponse sur le channel afin qu’elle soit traitée par une autre méthode. Nous y incrémentons aussi le « compteur d’attente » (WaitGroup) de goroutines, ce compteur sera décrémenté lorsque la réponse à la requête HTTP sera écrite dans le fichier.

func checkUrl(url string, verbose bool, responses chan string, wg *sync.WaitGroup) { // takes a channel and a pointer to WaitGroup as argument
 wg.Add(1) // We wait for this goroutine to end before closing channel (no need to explicitly dereference pointer to struct)
 resp, err := http.Head(url)
 if verbose {
  fmt.Printf("'%s', '%s', '%s',\n", url, resp.Status, resp.Header.Get("Content-Type"))
 }
 switch {
 case err != nil:
  responses <- fmt.Sprintf("%s, %s,\n", url, "KO") // An example of adding a response in the channel which will be read by the writeToFile function
 case resp.StatusCode != 200:
  responses <- fmt.Sprintf("%s, %s,\n", url, "KO")
 default:
  responses <- fmt.Sprintf("%s, %s,\n", url, "OK")
 }
}

writeToFile

Dans writeToFile() nous écrivons les réponses aux requêtes HTTP reçues via channels sur un fichier. On décrémente ensuite le compteur d’attente de goroutines.

func writeToFile(responses chan string, destUrl string, wg *sync.WaitGroup) { // Takes a pointer to WaitGroup and a channel as parameters
 destFile, err := os.Create(destUrl)
 defer destFile.Close()  // We will close the file at the end
 defer destFile.Sync()  // And Sync it just before
 if err != nil {
  log.Fatal(err)
 }
 for response := range responses { // Thanks to the magic range keyword we loop on the responses added in channel from other goroutines
  _, err := destFile.WriteString(response) // write to file, nothing special
  wg.Done() // We have wrote the responses in the file we can now close the channel if it is the last response
  if err != nil {
   log.Fatal(err)
  }
 }
}

Application

application() est la fonction principale du programme. La première partie de cette fonction est dédiée aux initialisations : l’ouverture du fichier contenant les URL à vérifier, la création du channel buferizée et la création du WaitGroup. La seconde partie correspond au lancement des différentes méthodes déjà évoquées : writeToFile et checkUrl. La dernière partie de la fonction application() attend que toutes les goroutines aient terminé leurs travaux pour fermer la channel, ce qui clôt la boucle de réception des réponses.

func application(srcUrl string, destUrl string, verbose bool) {
 // ---------------------------------- initialization
 srcFile, err := os.Open(srcUrl)
 defer srcFile.Close()
 if err != nil {
  log.Fatal(err)
 }
 responses := make(chan string, 10) // declaration of buffered channel with length of 10
 var wg sync.WaitGroup
 // ---------------------------------- execution
 go writeToFile(responses, destUrl, &wg) // magic go keyword for going concurrent
 scanner := bufio.NewScanner(srcFile)
 for scanner.Scan() {
  err := scanner.Err()
  if err != nil {
   log.Fatal(err)
  }
  go checkUrl(scanner.Text(), verbose, responses, &wg) // we pass the pointeur of wg with &
 }
 // ---------------------------------- finalization
 wg.Wait() // Wait for all responses to be written in file before continue
 close(responses) // We must close the channel in order to end the loop on channel responses in writeFile function
}

Main

La fonction main() utilise une excellente librairie (mow.cli) permettant de créer une application CLI facilement.

func main() {
 app := cli.App("cub", "Checks urls from a 'csv' FILE and write to a DEST") // utilization of mow.cli for creating a CLI app
 app.Version("v version", "0.0.1")

 var (
  verbose = app.BoolOpt("l logs", true, "Print result of requests")
  src = app.StringArg("SRC", "urls.csv", "File with urls")
  dest = app.StringArg("DEST", "urls-results.csv", "Destination file with results")
 )

 app.Action = func() {
  application(*src, *dest, *verbose) // here goes our code concerning checking URLs
 }

 app.Run(os.Args)
}

Conclusion

Avec une seule dépendance externe, 107 lignes de codes et des routines à go go, j’ai pu aider un ami. Le Go est un outil complet ; il met à disposition du développeur, directement dans le langage (goroutines) et dans la librairie par défaut (package sync) un moyen de répondre à des problématiques de concurrence.

Jean-Baptiste Petit
Jean-Baptiste est un développeur qui s’intéresse à un large panel de technologies aussi bien backend que frontend. Il est passionné par le monde de l’open-source et des technologies innovantes liées à l'écosystème Java et JVM. Il s’intéresse de près aux évolutions du web, des frameworks, des librairies qui permettent de concevoir des interfaces performantes et adaptées aux besoins des utilisateurs.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *