# Rux ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/rux?style=flat-square) [![Actions Status](https://github.com/gookit/rux/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/rux/actions) [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/gookit/rux)](https://github.com/gookit/rux) [![GoDoc](https://pkg.go.dev/badge/github.com/gookit/rux.svg)](https://pkg.go.dev/github.com/gookit/rux?tab=doc) [![Coverage Status](https://coveralls.io/repos/github/gookit/rux/badge.svg?branch=master)](https://coveralls.io/github/gookit/rux?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/rux)](https://goreportcard.com/report/github.com/gookit/rux) Simple and fast web framework for build golang HTTP applications. > [中文说明](README.zh-CN.md) ## v2 Highlights `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]Param` in `Context` for 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.HEAD` boilerplate) - 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](_benchmarks/v2-results.txt) for measured numbers, and [docs/MIGRATION-v1-to-v2.md](docs/MIGRATION-v1-to-v2.md) for breaking changes. ## Features **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 - `RESTful` and `Controller` style structs - Generic `http.Handler` middleware - Static file / directory / `embed.FS` serving - `NotFound`, `NotAllowed`, `Error`, and panic handlers **Built-in batteries (`server/`, `pkg/*`)** - Production-ready Server with sane timeouts, graceful shutdown, lifecycle hooks, and `/healthz` + `/readyz` endpoints ([docs](docs/echo-server.md)) - 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-aware `Responder` for JSON / XML / Text / HTML / Binary / Auto, with a pluggable `TemplateRenderer` interface (no template engine bundled) - Server-Sent Events (`pkg/sse`): `Stream` / `StreamWith` with lifecycle hooks, default `:connected` frame, optional keepalive, plus a `Hub` for keyed push and broadcast to active clients ## GoDoc - [godoc for github](https://pkg.go.dev/github.com/gookit/rux?tab=doc) ## Install ```bash go get github.com/gookit/rux/v2 ``` ## Quick start ```go package 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) } ``` ### Random port and the resolved address `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): ```go 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 yourself ``` `Listener()` 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](#production-ready-server)). ## Route Group Two forms with the same semantics (prefixes concatenate, middleware stacks): ```go // 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. ## Path Params 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 with `handlers.ParamRegex` (see the migration guide). ```go // 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}]`: ```go // 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) ``` ### Wildcards Catch-all wildcards capture everything past the prefix: ```go r.GET("/files/*path", func(c *rux.Context) { c.Text(200, "serve: "+c.Param("path")) }) ``` ## Use Middleware 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). `Use` may be called any time before the first request: global middleware is merged into every route chain when the router freezes, so a `Use` call also covers routes that were registered before it. Declaring it before your routes is still the clearest style (and the only option after the first request, which panics). Note that this makes `Use` inside a `Group` closure global, not group-scoped; pass middleware to `Group` for group scope. Examples: ```go 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**: ```text +-----------------------------+ | middle 1 | | +----------------------+ | | | middle 2 | | start | | +----------------+ | | end ------->| | | main handler | | |--->---- | | |________________| | | | |______________________| | |_____________________________| ``` > more please see [middleware_test.go](middleware_test.go) middleware tests ## Use http.Handler rux is support generic `http.Handler` interface middleware > You can use `rux.WrapHTTPHandler()` convert `http.Handler` as `rux.HandlerFunc` ```go 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)) } ``` ## More Usage ### Static Assets ```go 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)) } ``` ### Name Route 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: ```go 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 redirect to other page ```go 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() }) ``` ### Cookies you can quick operate cookies by `FastSetCookie()` `DelCookie()` > Note: You must set or delete cookies before writing BODY content ```go 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") }) ``` ### Multi Domains > code is refer from `julienschmidt/httprouter` ```go 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)) } ``` ### RESETFul Style ```go 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)) } ``` ### Controller Style ```go 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)) } ``` ### Build URL ```go 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)) } ``` ## Request Binding and Validation `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](https://github.com/gookit/validate), install an adapter once at startup: ```go 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. ## Production-Ready Server 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. ```go 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" // Global middleware. Use works any time before the first request; declaring // it up front is the clearest style. s.Use(myAuthMiddleware) // The routes, health checks included. s.GET("/", func(c *rux.Context) { c.Text(200, "hello") }) // Optional liveness/readiness endpoints under /healthz and /readyz. // The global chain covers them too. 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`, or `ServeTLS` when `TLSCertFile`/`TLSKeyFile` are set). Binding happens before the `PostStart` hooks run, so hooks always see the resolved address - Wait for `SIGINT` / `SIGTERM` (configurable via `StopSignals`) - On signal: flip `/readyz` to 503 → wait `DrainDelay` so the upstream LB can drain → call `http.Server.Shutdown` bounded by `ShutdownTimeout` - Run `PreShutdown` / `PostShutdown` hooks 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 | ### Ephemeral ports and the bound address 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: ```go 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. #### Serving on a listener you already own 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`: ```go 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. ### Echo Server (httpbin-style) `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)`. ```bash 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/upload ``` See [docs/echo-server.md](docs/echo-server.md) for the full endpoint table and usage recipes. ### Server-Sent Events `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. ```go 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`). Defaults at a glance (all of them live on `sse.Options`; `Stream` is shorthand for `StreamWith` with `nil` options): | Option | Default | Meaning | | ------------------- | ---------- | ------------------------------------------------ | | `SendConnected` | `true` | leading `: connected` frame | | `KeepaliveInterval` | `0` (off) | period of `: keepalive` frames when > 0 | | `Hooks` | `nil` | equivalent to an empty `&Hooks{}` | For keepalives use `StreamWith` and set `KeepaliveInterval`: ```go 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: ```go 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). ## Migrating from v1 If you are upgrading from rux v1.x, please read [docs/MIGRATION-v1-to-v2.md](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. ## Performance 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`](_benchmarks/v2-results.txt) for the benchmark numbers measured on the current branch. ## Help - lint ```bash golint ./... ``` - format check ```bash # list error files gofmt -s -l ./ # fix format and write to file gofmt -s -w some.go ``` - unit test ```bash go test -cover ./... ``` ## Gookit Packages - [gookit/ini](https://github.com/gookit/ini) Go config management, use INI files - [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP - [gookit/gcli](https://github.com/gookit/gcli) build CLI application, tool library, running CLI commands - [gookit/slog](https://github.com/gookit/slog) Concise and extensible go log library - [gookit/event](https://github.com/gookit/event) Lightweight event manager and dispatcher implements by Go - [gookit/cache](https://github.com/gookit/cache) Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached. - [gookit/config](https://github.com/gookit/config) Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags - [gookit/color](https://github.com/gookit/color) A command-line color library with true color support, universal API methods and Windows support - [gookit/filter](https://github.com/gookit/filter) Provide filtering, sanitizing, and conversion of golang data - [gookit/validate](https://github.com/gookit/validate) Use for data validation and filtering; rux can integrate it through `pkg/binding` without depending on it directly. - [gookit/goutil](https://github.com/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 ## See also - https://github.com/gin-gonic/gin - https://github.com/gorilla/mux - https://github.com/julienschmidt/httprouter - https://github.com/xialeistudio/go-dispatcher ## License **[MIT](LICENSE)**