Previously I shared a very basic example of Go routing. Let’s see how we can improve that a little.
In the previous version of the router I had a overly simplified approach, which required to check for the http method in the handler, to be able to handle different http methods. In order to improve that, I have introduced a simple router implementation. This allows for defining the routes, see the init
function in the server.go
file.
And here is the router itself:
package router
import (
"context"
"net/http"
"regexp"
"strings"
)
var Routes = []Route{}
type Route struct {
method string
regex *regexp.Regexp
handler http.HandlerFunc
}
type CtxKey struct{}
func CreateRoute(method, pattern string, handler http.HandlerFunc) Route {
return Route{method, regexp.MustCompile("^" + pattern + "$"), handler}
}
func Serve(w http.ResponseWriter, r *http.Request) {
var allow []string
for _, route := range Routes {
matches := route.regex.FindStringSubmatch(r.URL.Path)
if len(matches) > 0 {
if r.Method != route.method {
allow = append(allow, route.method)
continue
}
ctx := context.WithValue(r.Context(), CtxKey{}, matches[1:])
route.handler(w, r.WithContext(ctx))
return
}
}
if len(allow) > 0 {
w.Header().Set("Allow", strings.Join(allow, ", "))
http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
return
}
http.NotFound(w, r)
}
The server.go
has not changed that much beyond the router definitions. I introduced a config to handle environment variables more easily and in this particular case to determine if the server is running in development mode.
package main
import (
"embed"
"html/template"
"log"
"main/config"
"main/handlers"
"main/middlewares"
"main/router"
"net/http"
)
//go:embed templates
var embededTemplates embed.FS
//go:embed public
var embededPublic embed.FS
var reloaded = false
// public HTML route middleware stack
var publicHTMLStack = []middlewares.Middleware{
middlewares.Logger,
}
func init() {
var staticServer http.Handler
var stripPrefix string
router.Routes = []router.Route{
// HTML routes
router.CreateRoute("GET", "/", middlewares.CompileMiddleware(handlers.Home, publicHTMLStack)),
}
// only do this in development environment
if config.IsDevelopment() {
staticServer = http.FileServer(http.Dir("./public"))
stripPrefix = "/public/"
} else {
staticServer = http.FileServer(http.FS(embededPublic))
stripPrefix = "/"
}
router.Routes = append(router.Routes, router.CreateRoute("GET", "/public/.*", http.StripPrefix(stripPrefix, staticServer).ServeHTTP))
}
func main() {
// pre-parse templates, embedded in server binary
handlers.Tmpl = template.Must(template.ParseFS(embededTemplates, "templates/layouts/*.html", "templates/partials/*.html"))
// mux/router definition
mux := http.HandlerFunc(router.Serve)
// start the server
log.Fatal(http.ListenAndServe(":8000", mux))
}
I have also updated the middleware as the routing logic changed a bit, see the updated version below.
package middlewares
import (
"log"
"net/http"
"time"
)
// type for chaining
type Middleware func(http.HandlerFunc) http.HandlerFunc
// basically thisd is middleware chaining
func CompileMiddleware(h http.HandlerFunc, m []Middleware) http.HandlerFunc {
if len(m) < 1 {
return h
}
wrapped := h
// loop in reverse to preserve middleware order
for i := len(m) - 1; i >= 0; i-- {
wrapped = m[i](wrapped)
}
return wrapped
}
func Logger(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
If you would like to take a look at the code, you may find it here on GitHub.