DEV Community

sonatard
sonatard

Posted on

michi is a routing library designed for the Enhanced ServeMux in Go 1.22

Image description

I released michi. michi is a routing library designed for the Enhanced ServeMux in Go 1.22.

Image description

https://github.com/go-michi/michi

Sample

https://go.dev/play/p/wBWzrwkVD5j

Image description

Features

  • True 100% compatible with net/http - http.ServerMux, http.Handler and http.HandlerFunc
  • Enhanced http.ServeMux - After Go 1.22, it is possible to use http method and path values
  • API like chi - Route, Group, With and chi middlewares
  • No external dependencies - Only use standard package
  • Lightweight - Only 160 lines. michi only supports to setup handlers, routing is delegated to http.ServeMux
  • Performance - michi == http.ServeMux

About michi(道)

  • michi(道) means routes in Japanese.

Background

In versions before Go 1.21, net/http package didn't let you set HTTP methods or get path values from URL. This had to be done inside the handler, as seen here.

mux := http.NewServeMux()
// HTTP method and PathValue specification are not possible at handler setup
mux.Handle("/user/", func(w http.ResponseWriter, r *http.Request) {
    // Filtering by HTTP method must be implemented inside the handler
    if r.Method != http.MethodGet {
        w.WriteHeader(http.StatusMethodNotAllowed)
        return
    }
    // Implementing PathValue inside the handler
    // GET /user/12345 -> Hello 12345
    values := strings.Split(r.URL.Path, "/") // This is a simplified implementation
    w.Write([]byte("Hello " + values[len(values)-1]))
})
Enter fullscreen mode Exit fullscreen mode

To solve this, people used routing libraries like chi and gorilla/mux, and web frameworks such as echo and gin.

// chi
r := chi.NewRouter()
// HTTP method and PathValue can be specified at handler setup
r.Get("/user/{id}", func(w http.ResponseWriter, r *http.Request) {
    // Implementing HTTP method and PathValue inside the handler is unnecessary
    // GET /user/12345 -> Hello 12345
    w.Write([]byte("Hello " + chi.URLParam(r, "id")))
}) 
Go 1.22 solves this problem by allowing the specification of HTTP methods and retrieval of PathValues.goCopy codemux := http.NewServeMux()
mux.HandleFunc("GET /user/{id}", func(w http.ResponseWriter, r *http.Request) {
    // Implementing HTTP method and PathValue inside the handler is unnecessary
    // GET /user/12345 -> Hello 12345
    w.Write([]byte("Hello " + r.PathValue("id")))
})
Enter fullscreen mode Exit fullscreen mode

With the new features in http.ServeMux, it's easier to switch from chi to http.ServeMux. But, chi has easy-to-use interfaces for handlers and middleware that http.ServeMux doesn't have.

r := chi.NewRouter()
// Setting middleware with the Use method
r.Use(middleware.Logger)
// Nesting routes with the Route method
r.Route("/user", func(r chi.Router) {
    // Setting middleware specific to /user
    r.Use(middleware.UserLogger)
    r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello " + r.PathValue("id")))
    })
}
Enter fullscreen mode Exit fullscreen mode

michi was created to solve it. You get http.ServeMux's routing from the standard library and chi's ease of use.

In michi, as shown in the diagrams, routing uses http.ServeMux from the net/http package, not a custom implementation.


Usage

go get -u github.com/go-michi/michi
Enter fullscreen mode Exit fullscreen mode
package main

import (
    "fmt"
    "net/http"

    "github.com/go-chi/chi/v5/middleware"
    "github.com/go-michi/michi"
)

func main() {
    rt := michi.NewRouter()
    // Compatible with chi's interface, allowing the use of chi's middleware
    rt.Use(middleware.Logger)
    rt.Route("/user", func(rt michi.Router) {
        rt.HandleFunc("GET /{id}", func(w http.ResponseWriter, r *http.Request) {
            // GET /user/12345 -> Hello 12345
            w.Write([]byte("Hello " + r.PathValue("id")))
        })
    }
    http.ListenAndServe(":3000", rt)
}
Enter fullscreen mode Exit fullscreen mode

Before using michi, make sure to read the http.ServeMux documentation.
https://pkg.go.dev/net/http#ServeMux

For more detailed michi usage, check the GoDoc examples.
https://pkg.go.dev/github.com/go-michi/michi

differences betwean http.ServeMux and chi

1. Register method for HTTP Method

http.ServeMux and michi don't have register method for HTTP Method. use http.ServeMux handler rules.

// chi
r.Get("/user/{id}", userHandler)

// michi
r.HandleFunc("GET /user/{id}", userHandler)
Enter fullscreen mode Exit fullscreen mode

Chi might support it in the future. It seems to depend on the number of requests.
https://github.com/go-chi/chi/issues/895

2. Get Path Value

http.ServeMux and michi get path value from http.Request PathValue method.

// chi
id := chi.URLParam(r, "id")

// michi
id := r.PathValue("id")
Enter fullscreen mode Exit fullscreen mode

Chi will also support PathValue, so this difference will disappear.
https://github.com/go-chi/chi/issues/873

3. Handler pattern match rule

  • registers /a/ handler and requests /a/b
    • http.ServeMux and michi exec /a/ handler
    • prefix match
    • /a/b matches /a/.
    • chi returns 404 Not Found
    • exact match
    • /a/b does not match /a/.
  • after Go 1.22, registers /a/{$} and requests /a/b
    • http.ServeMux and michi returns 404 Not Found
    • exact match
    • /a/b does not match /a/.

4. Pattern match of value

Chi allows for pattern specification in ways that http.ServeMux and michi do not, such as:

  • Regular Expressions
    • /date/{yyyy:\d\d\d\d}/{mm:\d\d}/{dd:\d\d}
    • Example: /date/2017/04/01
  • Partial Matches
    • /user-{userID}
    • Example: /user-12345
  • Multiple Values
    • /{yyyy}-{mm}-{dd}
    • Example: /2024-10-10

5. Mount sub router

  • http.ServeMux requires adding sub router using Handle method because of it does not have a Mount method.
  • Handle method can't omit a parent handler path.
aRouter := http.NewServeMux()
// can't omit /a/ path
aRouter.HandleFunc("/a/hello", handler("hello"))
rt := http.NewServeMux()
rt.Handle("/a/", aRouter)
Enter fullscreen mode Exit fullscreen mode
  • chi adding sub router using Mount, it can omit a parent path.
aRouter := chi.NewRouter()
// omit /a/ path
aRouter.HandleFunc("/hello", handler("hello"))
rt := chi.NewRouter()
rt.Mount("/a", aRouter)
Enter fullscreen mode Exit fullscreen mode
  • michi is the sames as http.ServeMux.
aRouter := michi.NewRouter()
// can't omit /a/ path
aRouter.HandleFunc("/a/hello", handler("hello"))
rt := michi.NewRouter()
rt.Handle("/a/", aRouter)
Enter fullscreen mode Exit fullscreen mode

or using Route

func main() {
    r := michi.NewRouter()
    // can't omit /a/ path
    r.Route("/a", func(r michi.Router) {
        r.HandleFunc("/hello", handler("hello"))
    })
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Michi offers a balance between the enhanced functionality of Go 1.22's http.ServeMux and the convenience of chi. While I prefer using standard libraries where possible, chi still has advantages in convenience, making a transition to michi not always necessary. Choose based on your dependency and convenience requirements.

Credits

Top comments (0)