High-performance HTTP router for Go, with a focus on speed, simplicity, and ease-of-use.
go get -u github.com/yousuf64/shift
At the core of its performance, shift
uses a powerful combination of radix trees and hash maps, setting the standard for lightning-fast routing.
Why shift
?
shift
is faster than other mainstream HTTP routers.- Unlike other fast routers,
shift
strives to remain idiomatic and close to the standard library as much as possible. - Its primary focus is on routing requests quickly and efficiently, without attempting to become a full-fledged framework.
- Despite its simplicity,
shift
offers powerful routing capabilities. shift
is compatible withnet/http
request handlers and middlewares.
shift
is benchmarked against Gin and Echo in the benchmark suite.
The benchmark suite is also available as a GitHub Action.
Comparison between shift
, gin
and echo
as of Feb 27, 2023 on Go 1.19.4 (windows/amd64)
Benchmark system specifications:
- 12th Gen Intel Core i7-1265U vPro (12 MB cache, 10 cores, up to 4.80 GHz Turbo)
- 32 GB (2x 16 GB), DDR4-3200
- Windows 10 Enterprise 22H2
- Go 1.19.4 (windows/amd64)
BenchmarkShift_CaseInsensitiveAll-12 1750636 635.6 ns/op 0 B/op 0 allocs/op
BenchmarkGin_CaseInsensitiveAll-12 1000000 1066 ns/op 0 B/op 0 allocs/op
BenchmarkShift_GithubAll-12 79966 14575 ns/op 0 B/op 0 allocs/op
BenchmarkGin_GithubAll-12 49107 25962 ns/op 9911 B/op 154 allocs/op
BenchmarkEcho_GithubAll-12 54187 26318 ns/op 0 B/op 0 allocs/op
BenchmarkShift_GPlusAll-12 2492064 632.7 ns/op 0 B/op 0 allocs/op
BenchmarkGin_GPlusAll-12 1415556 837.9 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_GPlusAll-12 1000000 1154 ns/op 0 B/op 0 allocs/op
BenchmarkShift_OverlappingRoutesAll-12 923211 1174 ns/op 0 B/op 0 allocs/op
BenchmarkGin_OverlappingRoutesAll-12 352972 4029 ns/op 1953 B/op 32 allocs/op
BenchmarkEcho_OverlappingRoutesAll-12 552678 2310 ns/op 0 B/op 0 allocs/op
BenchmarkShift_ParseAll-12 1490170 838.6 ns/op 0 B/op 0 allocs/op
BenchmarkGin_ParseAll-12 748366 1492 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_ParseAll-12 697556 1829 ns/op 0 B/op 0 allocs/op
BenchmarkShift_RandomAll-12 817633 1241 ns/op 0 B/op 0 allocs/op
BenchmarkGin_RandomAll-12 292681 4675 ns/op 2201 B/op 34 allocs/op
BenchmarkEcho_RandomAll-12 428557 2717 ns/op 0 B/op 0 allocs/op
BenchmarkShift_StaticAll-12 452316 2595 ns/op 0 B/op 0 allocs/op
BenchmarkGin_StaticAll-12 128896 9701 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_StaticAll-12 106158 10877 ns/op 0 B/op 0 allocs/op
- Column 1: Benchmark name
- Column 2: Number of iterations, higher means more confident result
- Column 3: Nanoseconds elapsed per operation (ns/op), lower is better
- Column 4: Number of bytes allocated on heap per operation (B/op), lower is better
- Column 5: Average allocations per operation (allocs/op), lower is better
- Fast and zero heap allocations.
- Middleware support.
- Compatible with
net/http
request handlers and middlewares. - Route grouping.
- Allows declaring custom HTTP methods.
- Powerful routing system that includes:
- Route prioritization (Static > Param > Wildcard in that order).
- Case-insensitive route matching.
- Trailing slash with (or without) route matching.
- Path autocorrection.
- Allows conflicting/overlapping routes (
/posts/:id
and/posts/export
can exist together). - Allows different param names over the same path (
/users/:name
and/users/:id/delete
can exist without param name conflicts). - Mid-segment params (
/v:version/jobs
,/stream_*url
).
- Lightweight.
- Has zero external dependencies.
Minimum Go version: 1.18
To install shift
, simply run:
go get -u github.com/yousuf64/shift
Using shift
is easy. Here's a simple example:
package main
import (
"fmt"
"github.com/yousuf64/shift"
"net/http"
)
func main() {
router := shift.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, route shift.Route) error {
_, err := fmt.Fprint(w, "Hello, world!")
return err
})
http.ListenAndServe(":8080", router.Serve())
}
In this example, we create a shift
router, define a GET route for the root path, and start an HTTP server to listen for incoming requests on port 8080
.
shift
boasts a highly powerful and flexible routing system.
> Pattern: /foo
/foo match
/ no match
/foo/foo no match
> Pattern: /user/:name
/user/saul match
/user/saul/foo no match
/user/ no match
/user no match
> Pattern: /user:name
/usersaul match
/user no match
> Pattern: /user:fname:lname (not allowed, allows only one param within a segment '/.../')
> Pattern: /stream/*path
/stream/foo/bar/abc.mp4 match
/stream/foo match
/stream/ match
/stream no match
> Pattern: /stream*path
/streamfoo/bar/abc.mp4 match
/streamfoo match
/stream match
/strea no match
> Pattern: /*url*directory (not allowed, allows only one wildcard param per route)
shift
uses a slightly modified version of the net/http
request handler, which includes an additional parameter providing route information.
Moreover, the shift
request handler can return an error, making it convenient to handle errors in middleware without cluttering the handlers.
func(w http.ResponseWriter, r *http.Request, route shift.Route) error {
_, err := fmt.Fprintf(w, "Hello π")
return err
}
You can also use net/http
request handlers using HTTPHandlerFunc
adapter.
package main
import (
"fmt"
"github.com/yousuf64/shift"
"net/http"
)
func main() {
router := shift.New()
// Wrap the net/http handler in HTTPHandlerFunc
router.GET("/", shift.HTTPHandlerFunc(HelloHandler))
// ...
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "πππ")
}
To retrieve route information from a net/http
request handler, follow these steps:
- Attach the
RouteContext
middleware to the router, which will pack route information into thehttp.Request
context. - In the request handler, use the
RouteOf()
function to retrieve theRoute
object from thehttp.Request
context.
router := shift.New()
router.Use(shift.RouteContext())
router.GET("/hello/:name", shift.HTTPHandlerFunc(HelloUserHandler))
func HelloUserHandler(w http.ResponseWriter, r *http.Request) {
route := shift.RouteOf(r)
_, _ = fmt.Fprintf(w, "Hello, %s π from %s route", route.Params.Get("name"), route.Path)
// Writes 'Hello, Max π from /hello/:name route'
}
shift
supports both shift
-style and net/http
-style middlewares, allowing you to attach any net/http
compatible middleware.
- The
shift
middleware signature is:func(next shift.HandlerFunc) shift.HandlerFunc
- The
net/http
middleware signature is:func(next http.Handler) http.Handler
Middlewares can be scoped to all routes, to a specific group, or even to a single route.
func main() {
router := shift.New()
// Attaches to routes declared after Router.Use() statement.
router.Use(AuthMiddleware, shift.HTTPMiddlewareFunc(TraceMiddleware))
router.GET("/", Hello)
router.POST("/users", CreateUser)
// Attaches to routes declared within the group.
router.With(LoggerMiddleware).Group("/posts", PostsGroup)
// Attaches only to the chained route.
router.With(CORSMiddleware).GET("/comments", GetComments)
// ...
}
func AuthMiddleware(next shift.HandlerFunc) shift.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, route shift.Route) error {
// Authorization logic...
// You can conditionally circuit break from a middleware by returning before calling next().
if someCondition {
return nil
}
return next(w, r, route)
}
}
func TraceMiddleware(next http.Handler) http.Handler { ... }
func LoggerMiddleware(next shift.HandlerFunc) shift.HandlerFunc { ... }
func CORSMiddleware(next shift.HandlerFunc) shift.HandlerFunc { ... }
Note:
Router.Use()
can also be used within a group. It will attach the provided middlewares to the routes declared within the group after theRouter.Use()
statement.HTTPMiddlewareFunc
adapter can be used to attachnet/http
middleware.
Middleware handler | Description |
---|---|
RouteContext | Packs route information into http.Request context |
Recover | Gracefully handle panics |
Check out middleware examples.
By default, when a matching route is not found, it replies to the request with an HTTP 404 (Not Found) error.
Use Router.UseNotFoundHandler()
to register a custom not found handler.
router.UseNotFoundHandler(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(410) // Replies with a 410 error.
})
With this feature enabled, the router will check for matching routes for other HTTP methods when a matching route is not found.
If any are found, it replies with an HTTP 405 (Method Not Allowed) status code and includes the allowed methods in the Allow
header.
Use Router.UseMethodNotAllowedHandler()
to enable this feature.
router := shift.New()
router.UseMethodNotAllowedHandler()
router.GET("/cake", GetCakeHandler)
router.POST("/cake", PostCakeHandler)
On PUT /cake
request, since a PUT
route is not registered for the /cake
path,
the router will reply with an HTTP 405 (Method Not Allowed) status code and GET, POST
in the Allow
header.
Since shift
request handlers can return errors, it is easy to handle errors in middleware without cluttering the request handlers.
This helps to keep the request handlers clean and focused on their primary task.
Check out error handling examples.
When Router.UseTrailingSlashMatch()
is set, if the router is unable to find a match for the path, it tries to find a match with or without the trailing slash.
The routing behavior for the matched route is determined by the provided ActionOption
(See below).
When Router.UseTrailingSlashMatch()
is set, if the router is unable to find a match for the requested path, it will try to find a match with or without the trailing slash.
The routing behavior for the matched route is determined by the provided ActionOption
.
With shift.WithExecute()
option, the matched fallback route handler would be executed.
router := shift.New()
router.UseTrailingSlashMatch(shift.WithExecute())
router.GET("/foo", FooHandler) // Matches /foo and /foo/
router.GET("/bar/", BarHandler) // Matches /bar/ and /bar
In the above example, the first route handler matches both /foo
and /foo/
and the second route handler matches both /bar/
and /bar
.
When Router.UsePathCorrectionMatch()
is set, if the router is unable to find a match for the path, it will perform path correction and case-insensitive matching in order to find a match for the requested path.
The routing behavior for the matched route is determined by the provided ActionOption
.
With shift.WithRedirect()
option, it will return a HTTP 304 (Moved Permanently) status with a redirect to correct URL.
router := shift.New()
router.UsePathCorrectionMatch(shift.WithRedirect())
router.GET("/foo", FooHandler) // Matches /foo, /Foo, /fOO, /fOo, and so on...
router.GET("/bar/", BarHandler) // Matches /bar/, /Bar/, /bAr/, /BAR, /baR/, and so on...
Both UseTrailingSlashMatch
and UsePathCorrectionMatch
expects an ActionOption
which provides the routing behavior for the matched route, shift
provides three behavior providers:
WithExecute()
- Executes the request handler of the correct route.WithRedirect()
- Returns HTTP 304 (Moved Permanently) status with a redirect to correct URL in the header.WithRedirectCustom(statusCode)
- Is same asWithRedirect
, except it writes the provided status code (should be in range 3XX).
In a shift
style request handler, access route information such as the route path and route params directly through the Route
argument.
In a net/http
style request handler, attach the RouteContext
middleware and within the request handler, use RouteOf()
function to retrieve the Route
object.
When using Route
or Params
object in a Go Routine, make sure to get a clone using Copy()
which is available for both the objects.
func WorkerHandler(w http.ResponseWriter, r *http.Request, route shift.Route) error {
go FooWorker(route.Copy()) // Copies the whole Route object along with the internal Params object.
go BarWorker(route.Params.Copy()) // Copies only the Params object.
return nil
}
func FooWorker(route shift.Route) { ... }
func BarWorker(ps *shift.Params) { ... }
To register a request handler to multiple HTTP methods, use Router.Map()
.
router := shift.New()
router.Map([]string{"GET", "POST"}, "/zanzibar", func(w http.ResponseWriter, r *http.Request, route shift.Route) error {
_, err := fmt.Fprintf(w, "πππ")
return err
})
This is equivalent to registering a common request handler to the path /zanzibar
by calling both Router.GET()
and Router.POST()
.
You can also use Router.Map()
to register request handlers to custom HTTP methods.
router := shift.New()
router.Map([]string{"FOO"}, "/products", func(w http.ResponseWriter, r *http.Request, route shift.Route) error {
_, err := fmt.Fprintf(w, "Hello, from %s method π", r.Method)
return err
})
curl --request FOO --url '127.0.0.1:6464/products'
The router will reply Hello, from FOO method π
for the above request.
- Julien Schmidt for HttpRouter.
path.go
file is taken from theHttpRouter
project for path correction.
Licensed under MIT License
Copyright (c) 2023 Mohammed Yousuf
shift
is currently pre-1.0. Therefore, there could be minor breaking changes to stabilize the API before the initial stable release. Please open an issue if you have questions, requests, suggestions for improvements, or concerns.
It's intended to release 1.0.0 before the end of January 2024.