Simple and fast web framework for build golang HTTP applications.
rux v2 is a clean-room rewrite focused on extreme performance:
- High-performance Radix Tree routing — per-method tree, lock-free hot path
- Zero-allocation static routes (one map lookup per request)
- Inline
Params [16]ParaminContextfor low-allocation dynamic routing - Auto-freeze on first
ServeHTTP— the routing tables become read-only at runtime - Auto HEAD → GET mirror at freeze time (no manual
r.HEADboilerplate) - Pre-merged middleware chains (no per-request
append) - Same high-level API as v1 —
Router,Group,Resource,Controller,GET/POST/...all unchanged.
See _benchmarks/v2-results.txt for measured numbers, and docs/MIGRATION-v1-to-v2.md for breaking changes.
Routing & request handling
- Fast route match (Radix Tree, per-method, lock-free hot path), route groups
- Path params and named routing
- Route / group / global middleware
RESTfulandControllerstyle structs- Generic
http.Handlermiddleware - Static file / directory /
embed.FSserving NotFound,NotAllowed,Error, and panic handlers
Built-in batteries (server/, pkg/*)
- Production-ready Server with sane timeouts, graceful shutdown, lifecycle hooks, and
/healthz+/readyzendpoints (docs) - Echo Server: httpbin-style debug endpoints (
/anything,/status/{code},/delay,/redirect,/cookies,/basic-auth,/bytes,/uuid,/download,/upload, …) - Render package (
pkg/render): stateless helpers + status-awareResponderfor JSON / XML / Text / HTML / Binary / Auto, with a pluggableTemplateRendererinterface (no template engine bundled) - Server-Sent Events (
pkg/sse):Stream/StreamWithwith lifecycle hooks, default:connectedframe, optional keepalive, plus aHubfor keyed push and broadcast to active clients
go get github.com/gookit/rux/v2package main
import (
"fmt"
"github.com/gookit/rux/v2"
)
func main() {
r := rux.New()
// Add Routes:
r.GET("/", func(c *rux.Context) {
c.Text(200, "hello")
})
r.GET("/hello/{name}", func(c *rux.Context) {
c.Text(200, "hello "+c.Param("name"))
})
r.POST("/post", func(c *rux.Context) {
c.Text(200, "hello")
})
// add multi method support for a route path
r.Add("/post[/{id}]", func(c *rux.Context) {
if c.Param("id") == "" {
// do create post
c.Text(200, "created")
return
}
id := c.Params().Int("id")
// do update post
c.Text(200, "updated "+fmt.Sprint(id))
}, rux.POST, rux.PUT)
// Start server
r.Listen(":8080")
// can also
// http.ListenAndServe(":8080", r)
}Listen binds before it serves, so with an OS-assigned port (":0",
"127.0.0.1:0") the address it prints and reports is the real one. Use Bind
when you need the address before traffic starts, then hand the listener to
ServeListener (which blocks):
r := rux.New()
r.GET("/", home)
ln, err := r.Bind("127.0.0.1:0") // or Bind(":0") / Bind() for the default
if err != nil {
log.Fatal(err)
}
fmt.Println("listening on", r.ListenAddr()) // 127.0.0.1:50447
fmt.Println("port", r.ListenPort()) // 50447
r.ServeListener(ln) // blocks; also accepts a listener you created yourselfListener() returns the active listener (nil once the server exits) and Err()
reports the serve error. For graceful shutdown, health endpoints, lifecycle hooks
and a browser-ready LocalURL(), use the server package instead (see
Production-Ready Server).
Two forms with the same semantics (prefixes concatenate, middleware stacks):
// closure form: routes are registered on the outer router
r.Group("/articles", func() {
r.GET("", func(c *rux.Context) {
c.Text(200, "view list")
})
r.POST("", func(c *rux.Context) {
c.Text(200, "create ok")
})
r.GET(`/{id}`, func(c *rux.Context) {
c.Text(200, "view detail, id: "+c.Param("id"))
})
}, auth())
// value form (gin-style)
api := r.NewGroup("/api", auth())
api.GET("/users", listUsers) // GET /api/users, auth runs first
admin := api.NewGroup("/admin", isAdmin())
admin.DELETE("/users/{id}", deleteUser) // DELETE /api/admin/users/{id}Inside a request the order is global -> group (outer to inner) -> route -> handler.
g.Use(mw) adds middleware for routes registered after that call, g.Prefix() and
g.Router() expose the group context, and a group created inside a closure group
inherits its prefix and middleware. Like every other registration call, group
routes must be added before the first request.
In v2 the path-param syntax is {name} (named) or *name (wildcard).
Regex constraints such as
{id:\d+}are no longer supported — validate inside the handler or withhandlers.ParamRegex(see the migration guide).
// can access by: "/blog/123"
r.GET(`/blog/{id}`, func(c *rux.Context) {
id := c.Params().Int("id")
if id <= 0 {
c.AbortWithStatus(400)
return
}
c.Text(200, fmt.Sprintf("view detail, id: %d", id))
})
// or reject non-matching ids before the handler runs:
r.GET(`/users/{id}`, showUser, handlers.ParamRegex("id", `\d+`)) // 400 on "abc"optional params, like /about[.html] or /posts[/{id}]:
// can access by: "/blog/my-article" or "/blog/my-article.html"
r.GET(`/blog/{title}[.html]`, func(c *rux.Context) {
c.Text(200, "view detail, title: "+c.Param("title"))
})
r.Add("/posts[/{id}]", func(c *rux.Context) {
if c.Param("id") == "" {
// do create post
c.Text(200, "created")
return
}
id := c.Params().Int("id")
// do update post
c.Text(200, "updated "+fmt.Sprint(id))
}, rux.POST, rux.PUT)Catch-all wildcards capture everything past the prefix:
r.GET("/files/*path", func(c *rux.Context) {
c.Text(200, "serve: "+c.Param("path"))
})rux support use middleware, allow:
- global middleware
- group middleware
- route middleware
Call priority: global middleware -> group middleware -> route middleware
Fallback responses use the same order: the NotFound (404) and NotAllowed (405) handlers run after the global middleware chain, so authentication, security headers and request logging also cover unknown paths and method mismatches. Like Use, both must be registered before the first request (the composed chains are built when the router freezes).
Examples:
package main
import (
"fmt"
"github.com/gookit/rux/v2"
)
func main() {
r := rux.New()
// add global middleware
r.Use(func(c *rux.Context) {
// do something ...
})
// add middleware for the route
route := r.GET("/middle", func(c *rux.Context) { // main handler
c.WriteString("-O-")
}, func(c *rux.Context) { // middle 1
c.WriteString("a")
c.Next() // Notice: call Next()
c.WriteString("A")
// if call Abort(), will abort at the end of this middleware run
// c.Abort()
})
// add more by Use()
route.Use(func(c *rux.Context) { // middle 2
c.WriteString("b")
c.Next()
c.WriteString("B")
})
// now, access the URI /middle
// will output: ab-O-BA
}- Call sequence:
middle 1 -> middle 2 -> main handler -> middle 2 -> middle 1 - Flow chart:
+-----------------------------+
| middle 1 |
| +----------------------+ |
| | middle 2 | |
start | | +----------------+ | | end
------->| | | main handler | | |--->----
| | |________________| | |
| |______________________| |
|_____________________________|
more please see middleware_test.go middleware tests
rux is support generic http.Handler interface middleware
You can use
rux.WrapHTTPHandler()converthttp.Handlerasrux.HandlerFunc
package main
import (
"net/http"
"github.com/gookit/rux/v2"
// here we use gorilla/handlers, it provides some generic handlers.
"github.com/gorilla/handlers"
)
func main() {
r := rux.New()
// create a simple generic http.Handler
h0 := http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
w.Header().Set("new-key", "val")
})
r.Use(rux.WrapHTTPHandler(h0), rux.WrapHTTPHandler(handlers.ProxyHeaders()))
r.GET("/", func(c *rux.Context) {
c.Text(200, "hello")
})
// add routes ...
// Wrap our server with our gzip handler to gzip compress all responses.
http.ListenAndServe(":8000", handlers.CompressHandler(r))
}package main
import (
"embed"
"net/http"
"github.com/gookit/rux/v2"
)
//go:embed static
var embAssets embed.FS
func main() {
r := rux.New()
// one file
r.StaticFile("/site.js", "testdata/site.js")
// allow any files in the directory.
r.StaticDir("/static", "testdata")
// file type limit in the directory
r.StaticFiles("/assets", "testdata", "css|js")
// go 1.16+: use embed assets. access: /embed/static/some.html
r.StaticFS("/embed", http.FS(embAssets))
}In rux, you can add a named route, and you can get the corresponding route instance(rux.Route) from the router according to the name.
Examples:
r := rux.New()
// Method 1
myRoute := rux.NewNamedRoute("name1", "/path4/some/{id}", emptyHandler, "GET")
r.AddRoute(myRoute)
// Method 2
rux.AddNamed("name2", "/", func(c *rux.Context) {
c.Text(200, "hello")
})
// Method 3
r.GET("/hi", func(c *rux.Context) {
c.Text(200, "hello")
}).NamedTo("name3", r)
// get route by name
myRoute = r.GetRoute("name1")redirect to other page
r.GET("/", func(c *rux.Context) {
c.AbortThen().Redirect("/login", 302)
})
// Or
r.GET("/", func(c *rux.Context) {
c.Redirect("/login", 302)
c.Abort()
})
r.GET("/", func(c *rux.Context) {
c.Back()
c.Abort()
})you can quick operate cookies by FastSetCookie() DelCookie()
Note: You must set or delete cookies before writing BODY content
r.GET("/setcookie", func(c *rux.Context) {
c.FastSetCookie("rux_cookie2", "test-value2", 3600)
c.SetCookie("rux_cookie", "test-value1", 3600, "/", c.Req.URL.Host, false, true)
c.WriteString("hello, in " + c.URL().Path)
})
// FastSetCookie accepts optional func(*http.Cookie) callbacks to override
// the developer-friendly defaults — handy for HTTPS / SameSite.
r.GET("/setsecure", func(c *rux.Context) {
c.FastSetCookie("session", "v", 3600, func(ck *http.Cookie) {
ck.Secure = true
ck.SameSite = http.SameSiteStrictMode
})
})
r.GET("/delcookie", func(c *rux.Context) {
val := ctx.Cookie("rux_cookie") // "test-value1"
c.DelCookie("rux_cookie", "rux_cookie2")
})code is refer from
julienschmidt/httprouter
package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
type HostSwitch map[string]http.Handler
// Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if a http.Handler is registered for the given host.
// If yes, use it to handle the request.
if router := hs[r.Host]; router != nil {
router.ServeHTTP(w, r)
} else {
// Handle host names for which no handler is registered
http.Error(w, "Forbidden", 403) // Or Redirect?
}
}
func main() {
// Initialize a router as usual
router := rux.New()
router.GET("/", Index)
router.GET("/hello/{name}", func(c *rux.Context) {})
// Make a new HostSwitch and insert the router (our http handler)
// for example.com and port 12345
hs := make(HostSwitch)
hs["example.com:12345"] = router
// Use the HostSwitch to listen and serve on port 12345
log.Fatal(http.ListenAndServe(":12345", hs))
}package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
type Product struct {
}
// Uses middlewares [optional]
func (Product) Uses() map[string][]rux.HandlerFunc {
return map[string][]rux.HandlerFunc{
// function name: handlers
"Delete": []rux.HandlerFunc{
handlers.HTTPBasicAuth(map[string]string{"test": "123"}),
handlers.GenRequestID(),
},
}
}
// all products [optional]
func (p *Product) Index(c *rux.Context) { }
// create product [optional]
func (p *Product) Create(c *rux.Context) { }
// save new product [optional]
func (p *Product) Store(c *rux.Context) { }
// show product with {id} [optional]
func (p *Product) Show(c *rux.Context) { }
// edit product [optional]
func (p *Product) Edit(c *rux.Context) { }
// save edited product [optional]
func (p *Product) Update(c *rux.Context) { }
// delete product [optional]
func (p *Product) Delete(c *rux.Context) { }
func main() {
router := rux.New()
// methods Path Action Route Name
// GET /product index product_index
// GET /product/create create product_create
// POST /product store product_store
// GET /product/{id} show product_show
// GET /product/{id}/edit edit product_edit
// PUT/PATCH /product/{id} update product_update
// DELETE /product/{id} delete product_delete
// resetful style
router.Resource("/", new(Product))
log.Fatal(http.ListenAndServe(":12345", router))
}package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
// News controller
type News struct {
}
func (n *News) AddRoutes(g *rux.Router) {
g.GET("/", n.Index)
g.POST("/", n.Create)
g.PUT("/", n.Edit)
}
func (n *News) Index(c *rux.Context) { }
func (n *News) Create(c *rux.Context) { }
func (n *News) Edit(c *rux.Context) { }
func main() {
router := rux.New()
// controller style
router.Controller("/news", new(News))
log.Fatal(http.ListenAndServe(":12345", router))
}package main
import (
"log"
"net/http"
"github.com/gookit/rux/v2"
)
func main() {
// Initialize a router as usual
router := rux.New()
router.GET(`/news/{category_id}/{new_id}/detail`, func(c *rux.Context) {
var u = make(url.Values)
u.Add("username", "admin")
u.Add("password", "12345")
b := rux.NewBuildRequestURL()
// b.Scheme("https")
// b.Host("www.mytest.com")
b.Queries(u)
b.Params(rux.M{"{category_id}": "100", "{new_id}": "20"})
// b.Path("/dev")
// println(b.Build().String())
println(c.Router().BuildRequestURL("new_detail", b).String())
// result: /news/100/20/detail?username=admin&password=12345
// get current route name
if c.MustGet(rux.CTXCurrentRouteName) == "new_detail" {
// post data etc....
}
}).NamedTo("new_detail", router)
// Use the HostSwitch to listen and serve on port 12345
log.Fatal(http.ListenAndServe(":12345", router))
}pkg/binding supports form, query, header, JSON and XML binding. Validation is an extension hook and is disabled by default, so rux does not pull in a validation library unless your application chooses one.
To integrate gookit/validate, install an adapter once at startup:
package main
import (
"github.com/gookit/rux/v2/pkg/binding"
"github.com/gookit/validate"
)
type validateAdapter struct{}
func (validateAdapter) Validate(obj any) error {
v := validate.New(obj)
if v.Validate() {
return nil
}
return v.Errors.OneError()
}
func main() {
binding.Validator = validateAdapter{}
}After the adapter is installed, c.Bind, c.BindJSON, binding.Auto, and other built-in binders validate the bound object automatically. See _examples/validate for a runnable example.
Package server wraps a rux.Router with sensible HTTP timeouts, graceful shutdown, lifecycle hooks,
and built-in /healthz / /readyz endpoints. It is the recommended way to run rux in containers / k8s.
package main
import (
"context"
"log"
"github.com/gookit/rux/v2"
"github.com/gookit/rux/v2/server"
)
func main() {
s := server.New(false) // false = no debug logging
s.Addr = ":8080"
s.GET("/", func(c *rux.Context) {
c.Text(200, "hello")
})
// Optional liveness/readiness endpoints under /healthz and /readyz.
s.MountHealthChecks()
// Optional lifecycle hooks (warm caches, validate config, etc.).
s.PreStart = append(s.PreStart, func(ctx context.Context) error {
return nil
})
if err := s.Run(); err != nil {
log.Fatal(err)
}
}What Run() does for you:
- Bind the listener up front, then serve (
Serve, orServeTLSwhenTLSCertFile/TLSKeyFileare set). Binding happens before thePostStarthooks run, so hooks always see the resolved address - Wait for
SIGINT/SIGTERM(configurable viaStopSignals) - On signal: flip
/readyzto 503 → waitDrainDelayso the upstream LB can drain → callhttp.Server.Shutdownbounded byShutdownTimeout - Run
PreShutdown/PostShutdownhooks in order
Defaults tuned for container deployments:
| Field | Default | Purpose |
|---|---|---|
ReadHeaderTimeout |
2s | slowloris defense |
ReadTimeout |
10s | full request read budget |
WriteTimeout |
30s | response write budget |
IdleTimeout |
120s | keep-alive idle close |
DrainDelay |
5s | LB drain window after stop signal |
ShutdownTimeout |
25s | bound on graceful shutdown |
When Addr asks for port 0 ("127.0.0.1:0" or ":0") the kernel picks the
port. The resolved address is exposed through ListenAddr(), ListenPort() and
LocalURL(), and WaitListening(ctx) tells you when it is final (it waits for
the real bind, not a timer) or returns the startup error when binding fails:
s := server.New(false)
s.Addr = "127.0.0.1:0" // let the OS pick a free port
go func() { _ = s.Run() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.WaitListening(ctx); err != nil {
log.Fatal(err)
}
fmt.Println("listening on", s.ListenAddr()) // 127.0.0.1:50447
fmt.Println("open", s.LocalURL()) // http://127.0.0.1:50447
// e.g. launch the browser: github.com/gookit/goutil/sysutil
_ = sysutil.OpenURL(s.LocalURL())LocalURL() maps wildcard binds (":0", "0.0.0.0:0", "[::]:0") onto
127.0.0.1, uses https when TLS is configured, and returns "" while no
usable port exists, so an --open-style flag never receives a :0 or [::]
URL. Run() can only report errors, so WaitListening (or a PostStart hook,
which runs after the bind) is the supported way to learn the address.
When the socket is handed to you (systemd socket activation, a port pre-bound by
a test harness, a Unix socket), give it to the server instead of letting it bind
Addr:
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
s := server.New(false)
s.SetListener(ln) // Addr is ignored; Run() serves this listener
// or: s.ServeListener(ln) to block in the current goroutine without hooks
go func() { _ = s.Run() }()
if err := s.WaitListening(ctx); err == nil {
fmt.Println("listening on", s.ListenAddr()) // ln's address, reflected
}Listener() returns the active listener (nil when not listening). A listener
serves one run: http.Server closes it on shutdown, so a second Run needs a
fresh one.
server.NewEchoServer() builds a Server with httpbin-style debug endpoints pre-mounted:
/anything, /get|post|put|patch|delete, /status/{code}, /delay/{n}, /redirect/{n}, /cookies,
/basic-auth/{u}/{p}, /bytes/{n}, /uuid, /download/{filename}, POST /upload, and a /*path catch-all.
Useful for local debugging, integration tests, and as a /debug subtree inside larger apps via server.MountEchoRoutes(r).
go run ./_examples/echo-server
# then:
curl http://127.0.0.1:18080/anything
curl -F "file=@./README.md" http://127.0.0.1:18080/uploadSee docs/echo-server.md for the full endpoint table and usage recipes.
pkg/sse wraps the SSE wire format and lifecycle so handlers only have to drive the producer.
The Hooks struct exposes OnConnect / OnDisconnect / OnSend / OnError callbacks for auth, logging, filtering, and metrics — any field may be nil.
import "github.com/gookit/rux/v2/pkg/sse"
s.GET("/events", func(c *rux.Context) {
_ = sse.Stream(c, &sse.Hooks{
OnConnect: func(c *rux.Context) error { /* auth check */ return nil },
OnDisconnect: func(c *rux.Context, reason error) { /* audit */ },
}, func(send sse.SendFunc, done <-chan struct{}) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return nil
case t := <-ticker.C:
if err := send(sse.Event{Data: t.Format(time.RFC3339)}); err != nil {
return err
}
}
}
})
})OnConnect runs before the SSE headers are written, so a rejecting hook can issue any 4xx via c.Resp (e.g. http.Error(c.Resp, "no token", 401)).
Stream emits a leading : connected\n\n comment frame by default (suppress with StreamWith and SendConnected: false).
For keepalives use StreamWith and set KeepaliveInterval:
sse.StreamWith(c, &sse.Options{
Hooks: myHooks,
SendConnected: true,
KeepaliveInterval: 30 * time.Second, // ": keepalive\n\n" every 30s
}, producer)Two different timeouts, both matter:
| Timer | Handled by |
|---|---|
server.Server.WriteTimeout (default 30s) |
Automatic: Stream/StreamWith clear the write deadline for their response via http.ResponseController, so a stream is not cut mid-flight. |
| Proxy / NAT idle timeout (nginx 60s, ALB 60s, …) | KeepaliveInterval ≤ that value. |
Keyed push with Hub. For business-driven pushes (notify user X, broadcast to all) use sse.NewHub — an in-memory registry keyed by ID (e.g. user ID),
multi-connection-per-id (multi-tab fan-out), with non-blocking per-client buffer + dropped-event counter + OnDrop hook:
hub := sse.NewHub(64) // per-client buffer size
s.GET("/events", func(c *rux.Context) {
uid := authUserID(c)
_ = sse.Stream(c, nil, sse.HubProducer(hub, uid))
})
// elsewhere, business code:
delivered, dropped := hub.Send("user-42", sse.Event{Name: "notify", Data: "..."})
hub.Broadcast(sse.Event{Name: "announce", Data: "..."})
hub.SetOnDrop(func(c *sse.Client, _ sse.Event) {
log.Printf("slow client %s, dropped=%d", c.ID, c.Dropped())
})See _examples/sse-server for the full setup (subscribe + push + broadcast + stats endpoints with a tiny HTML demo client).
If you are upgrading from rux v1.x, please read docs/MIGRATION-v1-to-v2.md for a complete list of breaking changes. The high-level API surface is largely unchanged and most basic applications need no source edits.
rux v2 targets sub-200 ns/op for typical dynamic routes and 0 alloc/op
for static and most parametrized routes. See _benchmarks/v2-results.txt for the
benchmark numbers measured on the current branch.
- lint
golint ./...- format check
# list error files
gofmt -s -l ./
# fix format and write to file
gofmt -s -w some.go- unit test
go test -cover ./...- gookit/ini Go config management, use INI files
- gookit/rux Simple and fast request router for golang HTTP
- gookit/gcli build CLI application, tool library, running CLI commands
- gookit/slog Concise and extensible go log library
- gookit/event Lightweight event manager and dispatcher implements by Go
- gookit/cache Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached.
- gookit/config Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags
- gookit/color A command-line color library with true color support, universal API methods and Windows support
- gookit/filter Provide filtering, sanitizing, and conversion of golang data
- gookit/validate Use for data validation and filtering; rux can integrate it through
pkg/bindingwithout depending on it directly. - gookit/goutil Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more
- More please see https://github.com/gookit