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.