Dans le genre inutile donc indispensable, voici étape par étape l’écriture d’un petit serveur HTTP en Go qui servira des fichiers CSV dont le contenu est généré aléatoirement. L’objectif de cet article est plus de s’amuser un peu avec Go que d’être une référence en la matière, alors prenez le comme tel :).

Générer des fichiers CSV

Première étape, créer un fichier CSV :

package main

import (
    "encoding/csv"
    "log"
    "math/rand"
    "os"
)

func main() {
    records := generateCSV()
    if err := writeToFile("file.csv", records); err != nil {
        log.Fatal(err)
    }
}

main() fait d’abord appel à la fonction generateCSV(). Elle est responsable de générer le contenu du fichier et retourne un enregistrement CSV de 5 colonnes, 5 lignes et de mots de 8 caractères. Les lignes sont génerées par randomStringArray() :

func generateCSV() [][]string {
    cols := 5
    rows := 5
    wordSize := 8

    var records [][]string
    for i := 0; i < cols; i++ {
        records = append(records, randomStringArray(wordSize, rows))
    }
    return records
}

func randomStringArray(strLength, arrayLength int) []string {
    var letters = []rune("abcdefghijklmnopqrstuvwxyz")
    a := make([]string, arrayLength)
    for i := range a {
        b := make([]rune, strLength)
        for j := range b {
            b[j] = letters[rand.Intn(len(letters))]
        }
        a[i] = string(b)
    }
    return a
}

L’enregistrement est ensuite sauvé dans un fichier par writeToFile() :

func writeToFile(name string, records [][]string) error {
    file, err := os.Create(name)
    if err != nil {
        return err
    }

    w := csv.NewWriter(file)
    w.WriteAll(records)

    if err := w.Error(); err != nil {
        return err
    }
    return nil
}

Pour l’instant, notre petit programme Go est simplement capable d’écrire un fichier CSV au contenu aléatoire :

> go run main.go

> ls
file.csv main.go

> cat file.csv
xvlbzgba,icmrajww,hthctcua,xhxkqfda,fplsjfbc
xoeffrsw,xpldnjob,csnvlgte,mapezqle,qyhyzryw
jjpjzpfr,fegmotaf,ethsbzrj,xawnwekr,bemfdzdc
ekxbakjq,zlcttmtt,coanatyy,inkarekj,yixjrscc
tnswynsg,russvmao,zfzbsboj,ifqgzsnw,tksmvoig

On va légèrement modifier le main() pour permettre de générer plusieurs fichiers :

import (
    "encoding/csv"
    "fmt" // <- nouvel import
    "log"
    "math/rand"
    "os"
)

func main() {
    err := os.MkdirAll("content", 0755)
    if err != nil {
        log.Fatal(err)
    }

    filesCount := 10
    for i := 1; i <= filesCount; i++ {
        records := generateCSV()
        if err := writeToFile(fmt.Sprintf("content/file%03d.csv", i), records); err != nil {
            log.Fatal(err)
        }
    }
}

Un dossier content est créé dans lequel seront générés 10 fichiers CSV numérotés :

> go run main.go

> ls content
file001.csv ... file010.csv

Écrire le serveur

Il est maintenant temps d’écrire notre code serveur :

func serve(port string) error {
    http.HandleFunc("/random", func(res http.ResponseWriter, req *http.Request) {
        fileList, err := ioutil.ReadDir("content")
        if err != nil {
            return
        }

        file := fileList[rand.Intn(len(fileList))]

        if file.IsDir() {
            return
        }

        res.Header().Set("Random-Csv-Filename", file.Name())
        http.ServeFile(res, req, "content/"+file.Name())
    })
    http.Handle("/", http.FileServer(http.Dir("content")))
    log.Printf("Serving directory %s at :%s", "content", port)
    return http.ListenAndServe(":"+port, nil)
}

serve() prend un unique paramètre, le port d’écoute du serveur. La fonction définit un Handler qui est responsable de servir un fichier CSV sur la route /random.
Ce fichier est choisi aléatoirement par fileList[rand.Intn(len(fileList))] dans la liste de fichiers retournée par la fonction ioutil.ReadDir("content").

Ajoutons à main() l’appel à cette nouvelle fonction :

import (
    "encoding/csv"
    "fmt"
    "io/ioutil" // <- nouvel import
    "log"
    "math/rand"
    "net/http" // <- nouvel import
    "os"
)

func main() {
    err := os.MkdirAll("content", 0755)
    if err != nil {
        log.Fatal(err)
    }

    filesCount := 10
    for i := 1; i <= filesCount; i++ {
        records := generateCSV()
        if err := writeToFile(fmt.Sprintf("content/file%03d.csv", i), records); err != nil {
            log.Fatal(err)
        }
    }

    log.Fatal(serve("7000"))
}

Test

Le programme est complet, nous pouvons passer au test :

> go run main.go
2020/11/15 07:55:01 Serving directory content at :7000

Dans un autre terminal :

> curl http://localhost:7000/random
umzmgnwa,nyzawbxa,ddglszro,edscsmew,xilsdgpu
adyvtzdj,kfvhqxwa,uhwjelaw,lrwhrqdg,vudmlfjs
wkinxyeo,apfqfdos,ppvsozjk,hhhardeq,edmnxvgt
stcnpzro,rqtjgeaj,apjctdks,vwqjbcul,jbsaayri
ifjsqtnu,zvjlvwkd,yzcxuzqz,agyedzmp,ymihnwkg

> curl http://localhost:7000/random
llsrxbae,knvurjdm,vvexptzx,gwoughxj,fqoprfcg
jdythvqg,fbunopzu,ejgrtfpn,lkucvtwr,rapwrrto
sngivxkh,rnmwqszj,olhkgemj,fcmktdmk,qqrwtafx
xhlfalep,aaxwmbgv,zgdsyfbl,fgvozoag,ijikjvtm
taweccgc,wnaviqxy,qlaveqed,dcquaxxd,yaphjmnj

Et bien ça a l’air de fonctionner comme on le souhaite ! En effet, le serveur retourne un fichier différent à chaque appel… À un petit détail près, car si vous essayez de relancer le serveur, les appels curl nous renverront les mêmes données. C’est une histoire de seed, je vous laisse chercher…

Dernier détail, la réponse du serveur contient un header que nous avons défini dans le code, à savoir Random-Csv-Filename :

> curl -i http://localhost:7000/random
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 225
Content-Type: text/csv; charset=utf-8
Random-Csv-Filename: file082.csv

umzmgnwa,nyzawbxa,ddglszro,edscsmew,xilsdgpu
adyvtzdj,kfvhqxwa,uhwjelaw,lrwhrqdg,vudmlfjs
wkinxyeo,apfqfdos,ppvsozjk,hhhardeq,edmnxvgt
stcnpzro,rqtjgeaj,apjctdks,vwqjbcul,jbsaayri
ifjsqtnu,zvjlvwkd,yzcxuzqz,agyedzmp,ymihnwkg

Ce header peut être utilisé pour rappeler ce fichier si nous le souhaitons :

> curl -i http://localhost:7000/file082.csv
umzmgnwa,nyzawbxa,ddglszro,edscsmew,xilsdgpu
adyvtzdj,kfvhqxwa,uhwjelaw,lrwhrqdg,vudmlfjs
wkinxyeo,apfqfdos,ppvsozjk,hhhardeq,edmnxvgt
stcnpzro,rqtjgeaj,apjctdks,vwqjbcul,jbsaayri
ifjsqtnu,zvjlvwkd,yzcxuzqz,agyedzmp,ymihnwkg

Pour aller plus loin

En guise d’exercice, on pourrait implémenter les choses suivantes :

  • variabiliser le séparateur (par défaut c’est une ,)
  • générer le fichier CSV à la volée
  • passer cols, rows et wordSize en paramètres d’URL : curl http://localhost:7000/random?cols=5&rows=5&wordSize=8
  • ajouter une en-tête personnalisée
  • ajouter une seed pour que la fonction rand nous fournisse des CSV dans un ordre aléatoire à chaque lancement du serveur

Je vous laisse vous amuser avec ça, et poster vos propositions en commentaire ;)

Code complet

Si vous avez des suggestions, je les ajouterai au gist :