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 (

      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)
            ctx := context.WithValue(r.Context(), CtxKey{}, matches[1:])
            route.handler(w, r.WithContext(ctx))
        if len(allow) > 0 {
          w.Header().Set("Allow", strings.Join(allow, ", "))
          http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
        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 (

      //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{

      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 (

        // 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.