Better routing with Go’s standard library

published on February 17, 2023

Previously I shared a very basic example of Go routing. Let’s see how we can improve that a little.

Improving the routing definitions

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.