Insanely simple, idiomatic and fast - 200 loc
http router for golang
. Uses standard http.Handler and
has no limitations to path matching compared to routers
derived from Trie (radix) tree based solutions.
Less is exponentially more
fastroute.Router interface extends http.Handler with one extra method - Route in order to route http.Request to http.Handler allowing to chain routes until one is matched.
Go is about composition
The gravest problem all routers have - is the central structure holding all the context.
fastroute is extremely flexible, because it has only static, unbounded functions. Allows unlimited ways to compose router.
See the following example:
package main
import (
"fmt"
"net/http"
fr "github.com/DATA-DOG/fastroute"
)
var routes = map[string]fr.Router{
"GET": fr.Chain(
fr.New("/", handler),
fr.New("/hello/:name/:surname", handler),
fr.New("/hello/:name", handler),
),
"POST": fr.Chain(
fr.New("/users", handler),
fr.New("/users/:id", handler),
),
}
var router = fr.RouterFunc(func(req *http.Request) http.Handler {
return routes[req.Method] // fastroute.Router is also http.Handler
})
func main() {
http.ListenAndServe(":8080", router)
}
func handler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, fmt.Sprintf(
`%s "%s", pattern: "%s", parameters: "%v"`,
req.Method,
req.URL.Path,
fr.Pattern(req),
fr.Parameters(req),
))
}
In overall, it is not all in one router, it is the same http.Handler with do it yourself style, but with zero allocations path pattern matching. Feel free to just copy it and adapt to your needs.
It deserves a quote from Rob Pike:
Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don't get fancy.
The trade off this router makes is the size of n. Instead it provides orthogonal building blocks, just like http.Handler does, in order to build customized routers.
Here are some common usage guidelines:
Since fastroute.Router returns nil if request is not matched, we can easily extend it and create middleware for it at as many levels as we like.
package main
import (
"fmt"
"net/http"
"github.com/DATA-DOG/fastroute"
)
func main() {
notFoundHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(404)
fmt.Fprintln(w, "Ooops, looks like you mistyped the URL:", req.URL.Path)
})
router := fastroute.New("/users/:id", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, "user:", fastroute.Parameters(req).ByName("id"))
})
http.ListenAndServe(":8080", fastroute.RouterFunc(func(req *http.Request) http.Handler {
if h := router.Route(req); h != nil {
return h
}
return notFoundHandler
}))
}
This way, it is possible to extend fastroute.Router with various middleware, including:
- Method not found handler.
- Fixed path or trailing slash redirects. Based on your chosen route layout.
- Options or CORS.
In cases where n number of routes is very high and it is unknown what routes would be most frequently accessed or it changes during runtime, in order to highly improve performance, you can use hit count based reordering middleware.
package main
import (
"fmt"
"net/http"
"sort"
fr "github.com/DATA-DOG/fastroute"
)
var routes = map[string]fr.Router{
"GET": fr.Chain(
// here follows frequently accessed routes
HitCountingOrderedChain(
fr.New("/", handler),
fr.New("/health", handler),
fr.New("/status", handler),
),
// less frequently accessed routes
fr.New("/hello/:name/:surname", handler),
fr.New("/hello/:name", handler),
),
"POST": fr.Chain(
fr.New("/users", handler),
fr.New("/users/:id", handler),
),
}
// serves routes by request method
var router = fr.RouterFunc(func(req *http.Request) http.Handler {
return routes[req.Method] // fastroute.Router is also http.Handler
})
func main() {
http.ListenAndServe(":8080", router)
}
func HitCountingOrderedChain(routes ...fr.Router) fr.Router {
type HitCounter struct {
fr.Router
hits int64
}
hitRoutes := make([]*HitCounter, len(routes))
for i, r := range routes {
hitRoutes[i] = &HitCounter{Router: r}
}
return fr.RouterFunc(func(req *http.Request) http.Handler {
for i, r := range hitRoutes {
if h := r.Route(req); h != nil {
r.hits++
// reorder route hit is behind one third of routes
if i > len(hitRoutes)*30/100 {
sort.Slice(hitRoutes, func(i, j int) bool {
return hitRoutes[i].hits > hitRoutes[j].hits
})
}
return h
}
}
return nil
})
}
func handler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, fmt.Sprintf(
`%s "%s", pattern: "%s", parameters: "%v"`,
req.Method,
req.URL.Path,
fr.Pattern(req),
fr.Parameters(req),
))
}
Fastroute provides way to check whether request can be served, not only serve it. Though, the parameters then must be recycled in order to prevent leaking. When a routed request is served, it automatically recycles.
package main
import (
"fmt"
"net/http"
"strings"
fr "github.com/DATA-DOG/fastroute"
)
var routes = map[string]fr.Router{
"GET": fr.New("/users", handler),
"POST": fr.New("/users/:id", handler),
"PUT": fr.New("/users/:id", handler),
"DELETE": fr.New("/users/:id", handler),
}
var router = fr.RouterFunc(func(req *http.Request) http.Handler {
return routes[req.Method] // fastroute.Router is also http.Handler
})
var app = fr.RouterFunc(func(req *http.Request) http.Handler {
if h := router.Route(req); h != nil {
return h // routed and can be served
}
var allows []string
for method, routes := range routes {
if h := routes.Route(req); h != nil {
allows = append(allows, method)
fr.Recycle(req) // we will not serve it, need to recycle
}
}
if len(allows) == 0 {
return nil
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Allow", strings.Join(allows, ","))
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintln(w, http.StatusText(http.StatusMethodNotAllowed))
})
})
func main() {
http.ListenAndServe(":8080", app)
}
func handler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, fmt.Sprintf(
`%s "%s", pattern: "%s", parameters: "%v"`,
req.Method,
req.URL.Path,
fr.Pattern(req),
fr.Parameters(req),
))
}
If we make a request: curl -i http://localhost:8080/users/1
, we will get:
HTTP/1.1 405 Method Not Allowed
Allow: PUT,DELETE,POST
Date: Fri, 19 May 2017 06:09:56 GMT
Content-Length: 19
Content-Type: text/plain; charset=utf-8
Method Not Allowed
The best and fastest way to match static routes - is to have a map of path -> handler pairs.
package main
import (
"fmt"
"net/http"
"github.com/DATA-DOG/fastroute"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, req.URL.Path, fastroute.Parameters(req))
})
static := map[string]http.Handler{
"/status": handler,
"/users/roles": handler,
}
staticRoutes := fastroute.RouterFunc(func(req *http.Request) http.Handler {
return static[req.URL.Path]
})
dynamicRoutes := fastroute.Chain(
fastroute.New("/users/:id", handler),
fastroute.New("/users/:id/roles", handler),
)
http.ListenAndServe(":8080", fastroute.Chain(staticRoutes, dynamicRoutes))
}
In cases when your API faces public, it might be a good idea to redirect with corrected request URL if user makes a simple mistake.
This fixes trailing slash, case mismatch and cleaned path all at once. Note, we should follow some specific rule, how we build our path patterns in order to be able to fix them. In this case we follow all lowercase rule for static segments, parameters may match any case.
package main
import (
"fmt"
"net/http"
"path"
"strings"
"github.com/DATA-DOG/fastroute"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, req.URL.Path, fastroute.Parameters(req))
})
// we follow the lowercase rule for static segments
router := fastroute.Chain(
fastroute.New("/status", handler),
fastroute.New("/users/:id", handler),
fastroute.New("/users/:id/roles/", handler), // one with trailing slash
)
http.ListenAndServe(":8080", redirectTrailingOrFixedPath(router))
// requesting: http://localhost:8080/Users/5/Roles
// redirects: http://localhost:8080/users/5/roles/
}
func redirectTrailingOrFixedPath(router fastroute.Router) fastroute.Router {
return fastroute.RouterFunc(func(req *http.Request) http.Handler {
if h := router.Route(req); h != nil {
return h // has matched, no need for fixing
}
p := strings.ToLower(path.Clean(req.URL.Path)) // first clean path and lowercase
attempts := []string{p} // first variant with cleaned path
if p[len(p)-1] == '/' {
attempts = append(attempts, p[:len(p)-1]) // without trailing slash
} else {
attempts = append(attempts, p+"/") // with trailing slash
}
try, _ := http.NewRequest(req.Method, "/", nil) // make request for all attempts
for _, attempt := range attempts {
try.URL.Path = attempt
if h := router.Route(try); h != nil {
// matched, resolve fixed path and redirect
pat, params := fastroute.Pattern(try), fastroute.Parameters(try)
var fixed []string
var nextParam int
for _, segment := range strings.Split(pat, "/") {
if strings.IndexAny(segment, ":*") != -1 {
fixed = append(fixed, params[nextParam].Value)
nextParam++
} else {
fixed = append(fixed, segment)
}
}
fastroute.Recycle(try)
return redirect(strings.Join(fixed, "/"))
}
}
return nil // could not fix path
})
}
func redirect(fixedPath string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL.Path = fixedPath
http.Redirect(w, req, req.URL.String(), http.StatusPermanentRedirect)
})
}
This is trivial to implement a package inside your project, where all your routes used may be named. And later paths built by these named routes from anywhere within your application.
package routes
import (
"fmt"
"strings"
"github.com/DATA-DOG/fastroute"
)
var all = make(map[string]string)
func Named(name, path string, handler interface{}) fastroute.Router {
if p, dup := all[name]; dup {
panic(fmt.Sprintf(`route: "%s" at path: "%s" was already registered for path: "%s"`, name, path, p))
}
all[name] = path
return fastroute.New(path, handler)
}
func Get(name string, params fastroute.Params) string {
p, ok := all[name]
if !ok {
panic(fmt.Sprintf(`route: "%s" was never registered`, name))
}
for _, param := range params {
if key := ":" + param.Key; strings.Index(p, key) != -1 {
p = strings.Replace(p, key, param.Value, 1)
} else if key = "*" + param.Key; strings.Index(p, key) != -1 {
p = strings.Replace(p, key, param.Value, 1)
}
}
if strings.IndexAny(p, ":*") != -1 {
panic(fmt.Sprintf(`not all parameters were set: "%s" for route: "%s"`, p, name))
}
return p
}
Then the usage is obvious:
package main
import (
"fmt"
"net/http"
"github.com/DATA-DOG/fastroute"
"github.com/DATA-DOG/fastroute/routes" // should be somewhere in your project
)
func main() {
router := fastroute.Chain(
routes.Named("home", "/", handler),
routes.Named("hello-full", "/hello/:name/:surname", handler),
)
fmt.Println(routes.Get("hello-full", fastroute.Params{
{"name", "John"},
{"surname", "Doe"},
}))
http.ListenAndServe(":8080", router)
}
func handler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, fmt.Sprintf(`%s "%s"`, req.Method, req.URL.Path))
}
The benchmarks can be found here.
Benchmark type | repeats | cpu time op | mem op | mem allocs op |
---|---|---|---|---|
Gin_Param | 20000000 | 70.9 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_Param | 500000 | 3116 ns/op | 1056 B/op | 11 allocs/op |
HttpRouter_Param | 20000000 | 117 ns/op | 32 B/op | 1 allocs/op |
FastRoute_Param | 20000000 | 105 ns/op | 0 B/op | 0 allocs/op |
Pat_Param | 1000000 | 1910 ns/op | 648 B/op | 12 allocs/op |
Gin_Param5 | 20000000 | 117 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_Param5 | 300000 | 4597 ns/op | 1184 B/op | 11 allocs/op |
HttpRouter_Param5 | 3000000 | 487 ns/op | 160 B/op | 1 allocs/op |
FastRoute_Param5 | 20000000 | 117 ns/op | 0 B/op | 0 allocs/op |
Pat_Param5 | 300000 | 4717 ns/op | 964 B/op | 32 allocs/op |
Gin_Param20 | 5000000 | 280 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_Param20 | 200000 | 11062 ns/op | 3547 B/op | 13 allocs/op |
HttpRouter_Param20 | 1000000 | 1670 ns/op | 640 B/op | 1 allocs/op |
FastRoute_Param20 | 10000000 | 197 ns/op | 0 B/op | 0 allocs/op |
Pat_Param20 | 100000 | 20735 ns/op | 4687 B/op | 111 allocs/op |
Gin_ParamWrite | 10000000 | 170 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_ParamWrite | 500000 | 3136 ns/op | 1064 B/op | 12 allocs/op |
HttpRouter_ParamWrite | 10000000 | 156 ns/op | 32 B/op | 1 allocs/op |
FastRoute_ParamWrite | 10000000 | 162 ns/op | 0 B/op | 0 allocs/op |
Pat_ParamWrite | 500000 | 3196 ns/op | 1072 B/op | 17 allocs/op |
Gin_GithubStatic | 20000000 | 87.3 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GithubStatic | 100000 | 15215 ns/op | 736 B/op | 10 allocs/op |
HttpRouter_GithubStatic | 30000000 | 49.7 ns/op | 0 B/op | 0 allocs/op |
FastRoute_GithubStatic | 20000000 | 60.3 ns/op | 0 B/op | 0 allocs/op |
Pat_GithubStatic | 200000 | 10970 ns/op | 3648 B/op | 76 allocs/op |
Gin_GithubParam | 10000000 | 142 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GithubParam | 200000 | 9998 ns/op | 1088 B/op | 11 allocs/op |
HttpRouter_GithubParam | 5000000 | 301 ns/op | 96 B/op | 1 allocs/op |
FastRoute_GithubParam | 5000000 | 387 ns/op | 0 B/op | 0 allocs/op |
Pat_GithubParam | 200000 | 7083 ns/op | 2464 B/op | 48 allocs/op |
Gin_GithubAll | 50000 | 28162 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GithubAll | 300 | 5731742 ns/op | 211840 B/op | 2272 allocs/op |
HttpRouter_GithubAll | 30000 | 49198 ns/op | 13792 B/op | 167 allocs/op |
FastRoute_GithubAll | 10000 | 179753 ns/op | 5 B/op | 0 allocs/op |
Pat_GithubAll | 300 | 4388066 ns/op | 1499571 B/op | 27435 allocs/op |
Gin_GPlusStatic | 20000000 | 73.3 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GPlusStatic | 1000000 | 2015 ns/op | 736 B/op | 10 allocs/op |
HttpRouter_GPlusStatic | 30000000 | 34.0 ns/op | 0 B/op | 0 allocs/op |
FastRoute_GPlusStatic | 50000000 | 37.3 ns/op | 0 B/op | 0 allocs/op |
Pat_GPlusStatic | 5000000 | 330 ns/op | 96 B/op | 2 allocs/op |
Gin_GPlusParam | 20000000 | 96.9 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GPlusParam | 300000 | 4334 ns/op | 1056 B/op | 11 allocs/op |
HttpRouter_GPlusParam | 10000000 | 212 ns/op | 64 B/op | 1 allocs/op |
FastRoute_GPlusParam | 10000000 | 145 ns/op | 0 B/op | 0 allocs/op |
Pat_GPlusParam | 1000000 | 2142 ns/op | 688 B/op | 12 allocs/op |
Gin_GPlus2Params | 10000000 | 121 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GPlus2Params | 200000 | 8264 ns/op | 1088 B/op | 11 allocs/op |
HttpRouter_GPlus2Params | 10000000 | 232 ns/op | 64 B/op | 1 allocs/op |
FastRoute_GPlus2Params | 5000000 | 351 ns/op | 0 B/op | 0 allocs/op |
Pat_GPlus2Params | 200000 | 6557 ns/op | 2256 B/op | 34 allocs/op |
Gin_GPlusAll | 1000000 | 1279 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_GPlusAll | 20000 | 66580 ns/op | 13296 B/op | 142 allocs/op |
HttpRouter_GPlusAll | 1000000 | 2358 ns/op | 640 B/op | 11 allocs/op |
FastRoute_GPlusAll | 500000 | 2546 ns/op | 0 B/op | 0 allocs/op |
Pat_GPlusAll | 30000 | 47673 ns/op | 16576 B/op | 298 allocs/op |
Gin_ParseStatic | 20000000 | 71.2 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_ParseStatic | 500000 | 2971 ns/op | 752 B/op | 11 allocs/op |
HttpRouter_ParseStatic | 50000000 | 32.1 ns/op | 0 B/op | 0 allocs/op |
FastRoute_ParseStatic | 30000000 | 42.3 ns/op | 0 B/op | 0 allocs/op |
Pat_ParseStatic | 2000000 | 781 ns/op | 240 B/op | 5 allocs/op |
Gin_ParseParam | 20000000 | 79.2 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_ParseParam | 500000 | 3710 ns/op | 1088 B/op | 12 allocs/op |
HttpRouter_ParseParam | 10000000 | 181 ns/op | 64 B/op | 1 allocs/op |
FastRoute_ParseParam | 10000000 | 184 ns/op | 0 B/op | 0 allocs/op |
Pat_ParseParam | 500000 | 3165 ns/op | 1120 B/op | 17 allocs/op |
Gin_Parse2Params | 20000000 | 91.5 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_Parse2Params | 500000 | 3916 ns/op | 1088 B/op | 11 allocs/op |
HttpRouter_Parse2Params | 10000000 | 212 ns/op | 64 B/op | 1 allocs/op |
FastRoute_Parse2Params | 10000000 | 147 ns/op | 0 B/op | 0 allocs/op |
Pat_Parse2Params | 500000 | 2980 ns/op | 832 B/op | 17 allocs/op |
Gin_ParseAll | 1000000 | 2264 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_ParseAll | 10000 | 125569 ns/op | 24864 B/op | 292 allocs/op |
HttpRouter_ParseAll | 500000 | 3124 ns/op | 640 B/op | 16 allocs/op |
FastRoute_ParseAll | 500000 | 3324 ns/op | 0 B/op | 0 allocs/op |
Pat_ParseAll | 30000 | 56328 ns/op | 17264 B/op | 343 allocs/op |
Gin_StaticAll | 100000 | 19064 ns/op | 0 B/op | 0 allocs/op |
GorillaMux_StaticAll | 1000 | 1536755 ns/op | 115648 B/op | 1578 allocs/op |
FastRoute_StaticAll | 200000 | 9149 ns/op | 0 B/op | 0 allocs/op |
HttpRouter_StaticAll | 200000 | 10824 ns/op | 0 B/op | 0 allocs/op |
Pat_StaticAll | 1000 | 1597577 ns/op | 533904 B/op | 11123 allocs/op |
We can see that FastRoute outperforms fastest routers in some of the cases:
- Number of routes is small.
- Routes are static and served from a map.
- There are many named parameters.
FastRoute was easily adapted for this benchmark. Where static routes are served, nothing is better or faster than a static path map. FastRoute allows to build any kind of router, depending on an use case. By default it targets smaller number of routes and the weakest link is large set of dynamic routes, because these are matched one by one in order.
It always boils down to targeted case implementation. It is a general purpose router of 200 lines of source code in one file, which can be copied, understood and adapted in separate projects.
Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) - please open an issue before to discuss whether these changes can be accepted. All backward incompatible changes are and will be treated cautiously.
FastRoute is licensed under the three clause BSD license