@shakedko
IF AN EXPERT SAYS IT CAN'T BE DONE GET ANOTHER EXPERT.
- DAVID BEN-GURION

Serving static files with Go

Description

Lately I am trying to work on different projects while making sure that frontend and backend can work without any dependency.

Having working with this approach I can work on backend without having any frontend files in the same repository and vice versa. One of the things that needs to be done is setting the static files route and folder.

So how to do that with Go?

Suggested Solutions with Go

Plan

Lets say that we have a backend repository named RepoB and another one frontend RepoF. Now we want to load the static files from RepoF, e.g:

http://www.exampledomain.com/RepoF/styles/*.css
http://www.exampledomain.com/RepoF/scripts/*.js
http://www.exampledomain.com/RepoF/images/*.png|jpg|gif
http://www.exampledomain.com/RepoF/fonts/*.ttf

Basic Go solution, no external packages

Let say that we want to load our static files from /path/to/repofrontend/ by using http://localhost:9999/static/ as the static files URL:

package main
import (
        "net/http"
)
func main(){
        repoFrontend := "/path/to/repofrontend/"
        http.Handle("/static/",http.StripPrefix("/static/",
                http.FileServer(http.Dir(repoFrontend))))
        err := http.ListenAndServe(":9999", nil)
        if nil != err {
                panic(err)
        }
}

Playground

External package solution

Another solution that I have found in this post using an external package, Gorilla/Mux

Installation:

go get "github.com/gorilla/mux"

Snippet:

func ServeStatic(router *mux.Router, staticDirectory string) {
    staticPaths := map[string]string{
        "styles":           staticDirectory + "/styles/",
        "bower_components": staticDirectory + "/bower_components/",
        "images":           staticDirectory + "/images/",
        "scripts":          staticDirectory + "/scripts/",
    }
    for pathName, pathValue := range staticPaths {
        pathPrefix := "/" + pathName + "/"
        router.PathPrefix(pathPrefix).Handler(http.StripPrefix(pathPrefix,
            http.FileServer(http.Dir(pathValue))))
    }
}
router := mux.NewRouter()
staticDirectory := "/static/"
ServeStatic(router, staticDirectory)

Notice that using router.PathPrefix will solve the recursive issue above so for example you would be able to load your scripts by using /static/some/path/to/script.js

Manage Resources

My friend Carlos Cirello suggested and helped me to share a basic implementation of how to manage resources and make sure that the app knows how to handle max requests by using Go routines and channels. I have also used some example from golang-nuts.

The example shows how to manage resources and return status code 503 in case of requests overload and contains also a unit test implementationn.

Note that this example requires a valid path & file otherwise you would see status code 404

package main

import "net/http"

type limitHandler struct {
    connc   chan int
    handler http.Handler
}

func (h *limitHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    select {
    case <-h.connc:
        h.handler.ServeHTTP(w, req)
        h.connc <- 0
    default:
        http.Error(w, "503 too busy", http.StatusServiceUnavailable)
    }
}

func NewLimitHandler(maxConns int, handler http.Handler) http.Handler {
    h := &limitHandler{
        connc:   make(chan int, maxConns),
        handler: handler,
    }
    for i := 0; i < maxConns; i++ {
        h.connc <- i
    }
    return h
}

func InitHandler(handerPrefix, path string, maxConnections int) http.Handler {
    handler := http.FileServer(http.Dir(path))
    limitHandler := NewLimitHandler(maxConnections, handler)

    http.Handle(handerPrefix, http.StripPrefix(handerPrefix, limitHandler))

    return limitHandler
}

func main() {
    handerPrefix := "/static/"
    path := "./test-static/css/"
    maxConnections := 1
    InitHandler(handerPrefix, path, maxConnections)
    err := http.ListenAndServe(":9999", nil)
    if nil != err {
        panic(err)
    }
}

Unit test:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestStatusServiceUnavailable(t *testing.T) {
    connections := 1
    handerPrefix := "/static/"
    path := "./test-static/css/"
    cssFile := "/static/1/2/main.css"

    InitHandler(handerPrefix, path, connections)
    ts := httptest.NewServer(nil)
    defer ts.Close()

    url := ts.URL + cssFile
    aboveMaxConnections := (connections + 1)
    ch := make(chan *http.Response, aboveMaxConnections)
    queue := make(chan int)
    for i := 0; i < aboveMaxConnections; i++ {
        go func() {
            <-queue
            req, err := http.Get(url)
            if nil != err {
                t.Error(err)
            }
            ch <- req
        }()
    }

    for i := 0; i < aboveMaxConnections; i++ {
        queue <- i
    }

    return200Count := 0
    return503Count := 0
    for i := 0; i < aboveMaxConnections; i++ {
        res := <-ch
        if 200 == res.StatusCode {
            return200Count++
        }
        if 503 == res.StatusCode {
            return503Count++
        }
    }

    if return200Count != 1 || return503Count != 1 {
        t.Errorf("There should be 2 cases of response code 200 and 1 case of response code 503, got 200: %d, 503: %d", return200Count, return503Count)
    }
}