Because coding can also be pointless, here’s a step-by-step guide to writing a small HTTP server in Go that serves CSV files whose content are randomly generated. The point of this article is less to be a reference on the matter than to have a little fun while learning in Go, so take it as such :).

Generate files

First step is to programmatically create a CSV file:

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() first calls the generateCSV() function. It is responsible for generating the contents of the file and returns a CSV record of 5 columns, 5 lines and 8 character words. The lines are generated by 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
}

The record is then saved to a file by 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
}

For now, our little program just creates a CSV file with random content:

> 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

With slight changes, our code will now generate multiple files:

import (
    "encoding/csv"
    "fmt" // <- new 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)
        }
    }
}

A content folder is created in which there are 10 CSV files:

> go run main.go

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

The server

Now it’s time to write our server code:

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() takes a single parameter, the server port. The function defines a Handler that is responsible for serving a CSV file on the /random endpoint.
This file is randomly chosen by fileList[rand.Intn(len(fileList))] from the list of files returned by ioutil.ReadDir("content").

Let’s add to main() the call to this new function:

import (
    "encoding/csv"
    "fmt"
    "io/ioutil" // <- new import
    "log"
    "math/rand"
    "net/http" // <- new 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

The program is now complete, let’s test it:

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

And in another 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

Well it seems to work well! Indeed, the server returns a different file with each call… Except for one small detail: if you try to restart the server, the curl calls will return the same data. It’s because we didn’t initialize the randomness with a seed, but I’ll let you look into it…

Last detail, the server response contains a header that we defined in the code, namely 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

The header value can be used to call te returned file specifically:

> 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

Why stop now?

As an exercise, we could add more features to the already full-fledged, production-ready server :D ->

  • parameterize the separator (by default it is ,)
  • generate the CSV file on the fly when the HTTP call is made
  • pass cols, rows and wordSize as query parameters: curl http://localhost:7000/random?cols=5&rows=5&wordSize=8
  • add a custom header in the CSV file
  • add a seed to improve randomness

I let you have fun with that, and post your proposals in the comments ;)

Complete code

If you have any suggestions, I’ll add them to the gist :