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)
}
}
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"
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)
}
}