Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7eb25ddff | |||
| 788a736e06 | |||
| 3b94964068 | |||
| 1dba802590 |
177
COMPARISON.md
177
COMPARISON.md
@@ -1,177 +0,0 @@
|
|||||||
# Sux Router Comparison
|
|
||||||
|
|
||||||
This document compares Sux with other popular Go HTTP routers across various dimensions including performance, features, and ease of use.
|
|
||||||
|
|
||||||
## Performance Comparison
|
|
||||||
|
|
||||||
Based on benchmark tests and the original README data:
|
|
||||||
|
|
||||||
### Static Routes (ns/op)
|
|
||||||
- **Sux**: 798.5 ns/op (optimized with hash map)
|
|
||||||
- **httprouter**: ~900 ns/op (estimated from README)
|
|
||||||
- **gorilla/mux**: ~2000+ ns/op
|
|
||||||
- **chi**: ~1200 ns/op
|
|
||||||
- **gin**: ~1000 ns/op
|
|
||||||
|
|
||||||
### Memory Allocations
|
|
||||||
- **Sux**: 644 B/op, 8 allocs/op (static routes)
|
|
||||||
- **httprouter**: Similar allocation pattern
|
|
||||||
- **gorilla/mux**: Higher allocations due to regex matching
|
|
||||||
- **chi**: Moderate allocations
|
|
||||||
- **gin**: Higher allocations due to more features
|
|
||||||
|
|
||||||
**Sux Performance Advantage**: Hash map optimization provides O(1) child lookups vs O(n) linear search
|
|
||||||
|
|
||||||
## Feature Comparison
|
|
||||||
|
|
||||||
| Feature | Sux | httprouter | gorilla/mux | chi | gin |
|
|
||||||
|---------|-----|------------|-------------|-----|-----|
|
|
||||||
| Static Routes | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| URL Parameters (`:id`) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Wildcard Parameters (`*path`) | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
||||||
| Middleware Support | ✅ | ❌ | ❌ | ✅ | ✅ |
|
|
||||||
| Route Groups | ✅ | ❌ | ❌ | ✅ | ✅ |
|
|
||||||
| Method Not Allowed (405) | ✅ | ✅ | ❌ | ✅ | ✅ |
|
|
||||||
| Custom 404/405 Handlers | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
||||||
| Thread-Safe (no global state) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| Regex Support | ❌ | ❌ | ✅ | ❌ | ❌ |
|
|
||||||
| Subrouter Support | ✅ | ❌ | ✅ | ✅ | ✅ |
|
|
||||||
|
|
||||||
## Performance Analysis
|
|
||||||
|
|
||||||
### Sux Advantages:
|
|
||||||
1. **Minimal Allocations**: Uses sync.Pool for parameter maps, reducing GC pressure
|
|
||||||
2. **Hash Map Optimization**: O(1) child lookups vs O(n) linear search in other routers
|
|
||||||
3. **Efficient Trie Structure**: Optimized for fast static route matching
|
|
||||||
4. **Lightweight Middleware**: Minimal overhead for middleware chains
|
|
||||||
5. **Zero Dependencies**: No external dependencies, smaller binary size
|
|
||||||
|
|
||||||
### Where Others Excel:
|
|
||||||
1. **gorilla/mux**: More flexible regex patterns
|
|
||||||
2. **chi**: More mature middleware ecosystem
|
|
||||||
3. **gin**: More features and plugins
|
|
||||||
4. **httprouter**: Slightly better for pure static routing
|
|
||||||
|
|
||||||
## Real-World Performance
|
|
||||||
|
|
||||||
From the original README benchmarks:
|
|
||||||
|
|
||||||
```
|
|
||||||
Sux: 142,025.60 requests/sec
|
|
||||||
httprouter: 140,826.15 requests/sec
|
|
||||||
gomango: 110,315.36 requests/sec
|
|
||||||
gorilla: 108,078.84 requests/sec
|
|
||||||
```
|
|
||||||
|
|
||||||
Sux maintains excellent performance while adding more features than most competitors.
|
|
||||||
|
|
||||||
## Code Complexity
|
|
||||||
|
|
||||||
### Sux (Lines of Code)
|
|
||||||
- Core router: ~400 lines
|
|
||||||
- Tests: ~267 lines
|
|
||||||
- Total: ~667 lines
|
|
||||||
|
|
||||||
### Comparison
|
|
||||||
- **httprouter**: ~800 lines (core only)
|
|
||||||
- **gorilla/mux**: ~2000+ lines
|
|
||||||
- **chi**: ~1500+ lines
|
|
||||||
- **gin**: ~3000+ lines (full framework)
|
|
||||||
|
|
||||||
Sux achieves feature parity with significantly less code complexity.
|
|
||||||
|
|
||||||
## API Design Comparison
|
|
||||||
|
|
||||||
### Sux API
|
|
||||||
```go
|
|
||||||
r := sux.New()
|
|
||||||
r.GET("/users/:id", handler)
|
|
||||||
r.Use(middleware)
|
|
||||||
api := r.Group("/api", apiMiddleware)
|
|
||||||
```
|
|
||||||
|
|
||||||
### httprouter API
|
|
||||||
```go
|
|
||||||
r := httprouter.New()
|
|
||||||
r.GET("/users/:id", handler)
|
|
||||||
// No built-in middleware or groups
|
|
||||||
```
|
|
||||||
|
|
||||||
### gorilla/mux API
|
|
||||||
```go
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/users/{id}", handler).Methods("GET")
|
|
||||||
// No built-in middleware, requires third-party
|
|
||||||
```
|
|
||||||
|
|
||||||
### chi API
|
|
||||||
```go
|
|
||||||
r := chi.NewRouter()
|
|
||||||
r.Get("/users/{id}", handler)
|
|
||||||
r.Use(middleware)
|
|
||||||
r.Route("/api", func(r chi.Router) {
|
|
||||||
// Subroutes
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use Case Recommendations
|
|
||||||
|
|
||||||
### Choose Sux when:
|
|
||||||
- You need high performance with minimal dependencies
|
|
||||||
- You want middleware and route groups
|
|
||||||
- You prefer a simple, clean API
|
|
||||||
- Thread safety is important
|
|
||||||
- You need both static and parameter routes
|
|
||||||
|
|
||||||
### Choose httprouter when:
|
|
||||||
- You only need basic routing
|
|
||||||
- Maximum performance is the only concern
|
|
||||||
- You don't need middleware or groups
|
|
||||||
|
|
||||||
### Choose gorilla/mux when:
|
|
||||||
- You need complex regex patterns
|
|
||||||
- You're maintaining existing code that uses it
|
|
||||||
- You need maximum flexibility
|
|
||||||
|
|
||||||
### Choose chi when:
|
|
||||||
- You want a mature middleware ecosystem
|
|
||||||
- You prefer standard library compatibility
|
|
||||||
- You need extensive routing features
|
|
||||||
|
|
||||||
### Choose gin when:
|
|
||||||
- You want a full-featured framework
|
|
||||||
- You need extensive plugins and middleware
|
|
||||||
- You prefer batteries-included approach
|
|
||||||
|
|
||||||
## Migration Path
|
|
||||||
|
|
||||||
### From httprouter to Sux:
|
|
||||||
```go
|
|
||||||
// httprouter
|
|
||||||
r.GET("/users/:id", handler)
|
|
||||||
|
|
||||||
// Sux (almost identical)
|
|
||||||
r := sux.New()
|
|
||||||
r.GET("/users/:id", handler)
|
|
||||||
```
|
|
||||||
|
|
||||||
### From gorilla/mux to Sux:
|
|
||||||
```go
|
|
||||||
// gorilla/mux
|
|
||||||
r.HandleFunc("/users/{id}", handler).Methods("GET")
|
|
||||||
|
|
||||||
// Sux (simpler)
|
|
||||||
r.GET("/users/:id", handler)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Sux offers an excellent balance of performance, features, and simplicity:
|
|
||||||
|
|
||||||
1. **Performance**: Matches or exceeds all major routers
|
|
||||||
2. **Features**: Provides essential features (middleware, groups, parameters)
|
|
||||||
3. **Simplicity**: Clean API with minimal learning curve
|
|
||||||
4. **Maintainability**: Small codebase, easy to understand and modify
|
|
||||||
5. **Thread Safety**: No global state, safe for concurrent use
|
|
||||||
|
|
||||||
For most use cases requiring high performance with modern routing features, Sux provides an optimal solution that combines the best aspects of performance-focused and feature-rich routers.
|
|
||||||
302
README.md
302
README.md
@@ -1,268 +1,120 @@
|
|||||||
# Sux
|
# sux
|
||||||
|
|
||||||
Static route http router that considers the request method with support for parameters, middleware, and route groups.
|
An allocation-conscious, middleware-capable HTTP router for Go with support for static routes, parameters (`:id`), wildcards (`*path`), route groups, and configurable 404/405 handling.
|
||||||
|
|
||||||
Useful for serving server-side rendered content.
|
## Installation
|
||||||
|
|
||||||
## Features
|
```bash
|
||||||
|
go get code.icod.de/dalu/sux
|
||||||
|
```
|
||||||
|
|
||||||
- **High Performance**: Optimized trie-based routing with minimal allocations
|
## Quick start
|
||||||
- **URL Parameters**: Support for `:param` and `*wildcard` parameters
|
|
||||||
- **Middleware**: Global and route-specific middleware support
|
|
||||||
- **Route Groups**: Group routes with shared prefixes and middleware
|
|
||||||
- **Thread-Safe**: No global state - multiple router instances supported
|
|
||||||
- **Method Not Allowed**: Proper 405 responses when path exists but method doesn't
|
|
||||||
- **Custom Handlers**: Custom 404 and 405 handlers
|
|
||||||
|
|
||||||
## How
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.icod.de/dalu/sux"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"code.icod.de/dalu/sux"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
r := sux.New()
|
r := sux.New()
|
||||||
r.GET("/", Hello)
|
|
||||||
r.GET("/simple", Simple)
|
r.GET("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("hello"))
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := sux.ParamsFromContext(r)
|
||||||
|
w.Write([]byte("user " + params.Get("id")))
|
||||||
|
})
|
||||||
|
|
||||||
http.ListenAndServe(":8080", r)
|
http.ListenAndServe(":8080", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Hello(w http.ResponseWriter, r *http.Request) {
|
|
||||||
io.WriteString(w, "hello")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Simple(w http.ResponseWriter, r *http.Request) {
|
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
io.WriteString(w, "hello "+name)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### URL Parameters
|
## Middleware
|
||||||
|
|
||||||
|
Middleware wraps handlers and can be applied globally or per route group.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
r := sux.New()
|
logger := func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// log request here
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Named parameters
|
r := sux.New()
|
||||||
r.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
|
r.Use(logger) // global
|
||||||
params := sux.ParamsFromContext(r)
|
|
||||||
id := params["id"]
|
r.GET("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
io.WriteString(w, "User ID: "+id)
|
w.Write([]byte("pong"))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route groups
|
||||||
|
|
||||||
|
Groups share a prefix and middleware.
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := r.Group("/api", func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-API-Version", "v1")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
api.GET("/users", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("users"))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters and wildcards
|
||||||
|
|
||||||
|
```go
|
||||||
|
r.GET("/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
p := sux.ParamsFromContext(r)
|
||||||
|
w.Write([]byte(p.Get("postId")))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wildcard parameters (captures rest of path)
|
|
||||||
r.GET("/files/*path", func(w http.ResponseWriter, r *http.Request) {
|
r.GET("/files/*path", func(w http.ResponseWriter, r *http.Request) {
|
||||||
params := sux.ParamsFromContext(r)
|
p := sux.ParamsFromContext(r)
|
||||||
path := params["path"]
|
w.Write([]byte("file: " + p.Get("path")))
|
||||||
io.WriteString(w, "File path: "+path)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Multiple parameters
|
|
||||||
r.GET("/users/:userId/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
params := sux.ParamsFromContext(r)
|
|
||||||
userId := params["userId"]
|
|
||||||
postId := params["postId"]
|
|
||||||
io.WriteString(w, "User "+userId+", Post "+postId)
|
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Middleware
|
## 404/405 handlers
|
||||||
|
|
||||||
```go
|
```go
|
||||||
r := sux.New()
|
|
||||||
|
|
||||||
// Global middleware
|
|
||||||
r.Use(loggingMiddleware, authMiddleware)
|
|
||||||
|
|
||||||
// Route-specific middleware
|
|
||||||
r.GET("/admin", adminOnlyMiddleware(adminHandler))
|
|
||||||
|
|
||||||
func loggingMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s", r.Method, r.URL.Path)
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func authMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Check authentication
|
|
||||||
if !isAuthenticated(r) {
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Route Groups
|
|
||||||
|
|
||||||
```go
|
|
||||||
r := sux.New()
|
|
||||||
|
|
||||||
// API group with middleware
|
|
||||||
api := r.Group("/api", apiVersionMiddleware, corsMiddleware)
|
|
||||||
|
|
||||||
// Group routes automatically get the prefix and middleware
|
|
||||||
api.GET("/users", listUsers)
|
|
||||||
api.POST("/users", createUser)
|
|
||||||
api.GET("/users/:id", getUser)
|
|
||||||
|
|
||||||
// Nested groups
|
|
||||||
v1 := api.Group("/v1")
|
|
||||||
v1.GET("/posts", listPostsV1)
|
|
||||||
|
|
||||||
v2 := api.Group("/v2")
|
|
||||||
v2.GET("/posts", listPostsV2)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Handlers
|
|
||||||
|
|
||||||
```go
|
|
||||||
r := sux.New()
|
|
||||||
|
|
||||||
// Custom 404 handler
|
|
||||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
http.Error(w, "custom 404", http.StatusNotFound)
|
||||||
w.Write([]byte("Custom 404 - Page not found"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Custom 405 handler
|
|
||||||
r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
|
r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
http.Error(w, "custom 405", http.StatusMethodNotAllowed)
|
||||||
w.Write([]byte("Custom 405 - Method not allowed"))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Disable cross-method probing if you prefer 404s instead of 405 checks
|
||||||
|
r.EnableMethodNotAllowedCheck(false)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multiple Router Instances
|
## Performance notes
|
||||||
|
|
||||||
```go
|
- Internally pools parsed segments and parameter storage to reduce per-request allocations.
|
||||||
// Each router is independent and thread-safe
|
- Parameters are copied into the request context so they remain valid even after handlers return (important for async users of the context).
|
||||||
mainRouter := sux.New()
|
- For raw numbers, run `go test -bench . -benchmem`.
|
||||||
mainRouter.GET("/", homeHandler)
|
|
||||||
|
|
||||||
apiRouter := sux.New()
|
## Testing
|
||||||
apiRouter.GET("/users", usersHandler)
|
|
||||||
|
|
||||||
// Use different routers for different purposes
|
```bash
|
||||||
go http.ListenAndServe(":8080", mainRouter)
|
go test ./...
|
||||||
go http.ListenAndServe(":8081", apiRouter)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance
|
Benchmarks:
|
||||||
|
|
||||||
The router is optimized for high performance with hash map-based O(1) child lookups and zero-allocation optimizations:
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -bench . -benchmem
|
||||||
```
|
```
|
||||||
BenchmarkStaticRoute-8 1859550 682.1 ns/op 740 B/op 10 allocs/op
|
|
||||||
BenchmarkParameterRoute-8 1538643 868.9 ns/op 704 B/op 9 allocs/op
|
|
||||||
BenchmarkWildcardRoute-8 1281626 979.3 ns/op 736 B/op 10 allocs/op
|
|
||||||
BenchmarkMultipleParameters-8 1000000 1026 ns/op 768 B/op 9 allocs/op
|
|
||||||
BenchmarkMiddleware-8 567559 1930 ns/op 1568 B/op 19 allocs/op
|
|
||||||
BenchmarkRouteGroups-8 867961 1442 ns/op 1480 B/op 15 allocs/op
|
|
||||||
BenchmarkLargeRouter-8 1548286 833.0 ns/op 704 B/op 9 allocs/op
|
|
||||||
```
|
|
||||||
|
|
||||||
**Performance Improvements:**
|
|
||||||
- Static routes: 14.6% faster (682.1 vs 798.5 ns/op)
|
|
||||||
- Parameter routes: 24.7% faster (868.9 vs 1154 ns/op)
|
|
||||||
- Wildcard routes: 41.6% faster (979.3 vs 1676 ns/op)
|
|
||||||
- Multiple parameters: 41.5% faster (1026 vs 1753 ns/op)
|
|
||||||
- Middleware: 49.0% faster (1930 vs 3782 ns/op)
|
|
||||||
- Route groups: 49.5% faster (1442 vs 2855 ns/op)
|
|
||||||
- Large router: 24.5% faster (833.0 vs 1103 ns/op)
|
|
||||||
|
|
||||||
**Memory Allocation Improvements:**
|
|
||||||
- Optimized parameter handling with slice-based storage
|
|
||||||
- Zero-allocation path parsing for common cases
|
|
||||||
- Enhanced memory pooling for frequently used objects
|
|
||||||
- String builder optimization for wildcard parameters
|
|
||||||
|
|
||||||
### Performance Comparison
|
|
||||||
|
|
||||||
Compared to other popular Go routers:
|
|
||||||
|
|
||||||
```
|
|
||||||
darko@arch ~ $ wrk -c1000 -t8 -d30s http://inuc:8080/
|
|
||||||
Running 30s test @ http://inuc:8080/
|
|
||||||
8 threads and 1000 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 7.08ms 3.02ms 64.38ms 89.40%
|
|
||||||
Req/Sec 18.24k 2.11k 24.06k 73.88%
|
|
||||||
4260394 requests in 30.00s, 491.63MB read
|
|
||||||
Requests/sec: 142025.60
|
|
||||||
Transfer/sec: 16.39MB
|
|
||||||
```
|
|
||||||
|
|
||||||
Compared to https://github.com/julienschmidt/httprouter
|
|
||||||
```
|
|
||||||
darko@arch ~ $ wrk -c1000 -t8 -d30s http://inuc:8080/
|
|
||||||
Running 30s test @ http://inuc:8080/
|
|
||||||
8 threads and 1000 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 7.14ms 3.13ms 74.06ms 89.99%
|
|
||||||
Req/Sec 18.18k 2.19k 23.31k 75.88%
|
|
||||||
4224358 requests in 30.00s, 487.47MB read
|
|
||||||
Requests/sec: 140826.15
|
|
||||||
Transfer/sec: 16.25MB
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### Router Methods
|
|
||||||
|
|
||||||
- `New() *Router` - Creates a new router instance
|
|
||||||
- `GET(path string, handler http.HandlerFunc) *Router`
|
|
||||||
- `POST(path string, handler http.HandlerFunc) *Router`
|
|
||||||
- `PUT(path string, handler http.HandlerFunc) *Router`
|
|
||||||
- `PATCH(path string, handler http.HandlerFunc) *Router`
|
|
||||||
- `DELETE(path string, handler http.HandlerFunc) *Router`
|
|
||||||
- `OPTIONS(path string, handler http.HandlerFunc) *Router`
|
|
||||||
- `HEAD(path string, handler http.HandlerFunc) *Router`
|
|
||||||
|
|
||||||
### Middleware
|
|
||||||
|
|
||||||
- `Use(middleware ...MiddlewareFunc)` - Add global middleware
|
|
||||||
- `Group(prefix string, middleware ...MiddlewareFunc) *Router` - Create route group
|
|
||||||
|
|
||||||
### Custom Handlers
|
|
||||||
|
|
||||||
- `NotFound(handler http.HandlerFunc)` - Set custom 404 handler
|
|
||||||
- `MethodNotAllowed(handler http.HandlerFunc)` - Set custom 405 handler
|
|
||||||
|
|
||||||
### Parameter Extraction
|
|
||||||
|
|
||||||
- `ParamsFromContext(r *http.Request) Params` - Extract route parameters from request
|
|
||||||
|
|
||||||
## Parameter Types
|
|
||||||
|
|
||||||
### Named Parameters (`:param`)
|
|
||||||
|
|
||||||
- Matches a single path segment
|
|
||||||
- Extracted by name from the context
|
|
||||||
- Example: `/users/:id` matches `/users/123`
|
|
||||||
|
|
||||||
### Wildcard Parameters (`*param`)
|
|
||||||
|
|
||||||
- Matches one or more path segments
|
|
||||||
- Captures the rest of the path
|
|
||||||
- Example: `/files/*path` matches `/files/docs/readme.txt`
|
|
||||||
|
|
||||||
## Thread Safety
|
|
||||||
|
|
||||||
The router is completely thread-safe:
|
|
||||||
|
|
||||||
- No global state
|
|
||||||
- Multiple router instances can be used concurrently
|
|
||||||
- Safe for concurrent use from multiple goroutines
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License - see LICENSE file for details.
|
|
||||||
|
|||||||
@@ -198,3 +198,23 @@ func BenchmarkPoolEfficiency(b *testing.B) {
|
|||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BenchmarkContextParamCopy measures overhead of copying params into context
|
||||||
|
func BenchmarkContextParamCopy(b *testing.B) {
|
||||||
|
router := New()
|
||||||
|
router.GET("/users/:id/posts/:postId", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := ParamsFromContext(r)
|
||||||
|
_ = params.Get("id") + params.Get("postId")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/users/123/posts/456", nil)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
208
sux.go
208
sux.go
@@ -3,7 +3,6 @@ package sux
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,8 +43,8 @@ type (
|
|||||||
methodNotAllowedHandler http.HandlerFunc
|
methodNotAllowedHandler http.HandlerFunc
|
||||||
paramsPool *sync.Pool // Use pointer to avoid copying
|
paramsPool *sync.Pool // Use pointer to avoid copying
|
||||||
segmentsPool *sync.Pool // Pool for path segments
|
segmentsPool *sync.Pool // Pool for path segments
|
||||||
builderPool *sync.Pool // Pool for string builders
|
|
||||||
prefix string // For route groups
|
prefix string // For route groups
|
||||||
|
checkMethodNotAllowed bool // Whether to probe other methods on 404
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,8 +55,13 @@ const paramsKey contextKey = "params"
|
|||||||
|
|
||||||
// ParamsFromContext extracts route parameters from the request context
|
// ParamsFromContext extracts route parameters from the request context
|
||||||
func ParamsFromContext(r *http.Request) Params {
|
func ParamsFromContext(r *http.Request) Params {
|
||||||
if params, ok := r.Context().Value(paramsKey).(Params); ok {
|
switch params := r.Context().Value(paramsKey).(type) {
|
||||||
|
case Params:
|
||||||
return params
|
return params
|
||||||
|
case *Params:
|
||||||
|
if params != nil {
|
||||||
|
return *params
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Params{}
|
return Params{}
|
||||||
}
|
}
|
||||||
@@ -148,7 +152,7 @@ func parsePath(path string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
segments := make([]string, 0, 4) // Pre-allocate for common cases
|
segments := make([]string, 0, 4) // Pre-allocate for common cases
|
||||||
start := 1 // Skip the leading slash
|
start := 1 // Skip the leading slash
|
||||||
|
|
||||||
for i := 1; i < len(path); i++ {
|
for i := 1; i < len(path); i++ {
|
||||||
if path[i] == '/' {
|
if path[i] == '/' {
|
||||||
@@ -166,6 +170,31 @@ func parsePath(path string) []string {
|
|||||||
return segments
|
return segments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsePathInto splits a path into segments using the provided buffer
|
||||||
|
func parsePathInto(path string, buf []string) []string {
|
||||||
|
if path == "" || path == "/" {
|
||||||
|
return buf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = buf[:0]
|
||||||
|
start := 1 // Skip the leading slash
|
||||||
|
|
||||||
|
for i := 1; i < len(path); i++ {
|
||||||
|
if path[i] == '/' {
|
||||||
|
if start < i {
|
||||||
|
buf = append(buf, path[start:i])
|
||||||
|
}
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start < len(path) {
|
||||||
|
buf = append(buf, path[start:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
// parsePathZeroAlloc splits a path into segments with minimal allocations
|
// parsePathZeroAlloc splits a path into segments with minimal allocations
|
||||||
// Uses string slicing to avoid allocations where possible
|
// Uses string slicing to avoid allocations where possible
|
||||||
func parsePathZeroAlloc(path string) []string {
|
func parsePathZeroAlloc(path string) []string {
|
||||||
@@ -249,7 +278,6 @@ func (n *node) addRouteWithMiddleware(segments []string, handler http.HandlerFun
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isLast {
|
if isLast {
|
||||||
current.nodeType = static
|
|
||||||
current.handler = handler
|
current.handler = handler
|
||||||
current.middleware = middleware
|
current.middleware = middleware
|
||||||
}
|
}
|
||||||
@@ -257,11 +285,14 @@ func (n *node) addRouteWithMiddleware(segments []string, handler http.HandlerFun
|
|||||||
}
|
}
|
||||||
|
|
||||||
// find searches for a route matching the given path
|
// find searches for a route matching the given path
|
||||||
func (n *node) find(segments []string, params *Params) *node {
|
func (n *node) find(path string, segments []string, params *Params) *node {
|
||||||
current := n
|
current := n
|
||||||
|
pos := 1 // Track offset into the original path for wildcard slicing
|
||||||
|
|
||||||
for i, segment := range segments {
|
for i, segment := range segments {
|
||||||
isLast := i == len(segments)-1
|
isLast := i == len(segments)-1
|
||||||
|
segmentStart := pos
|
||||||
|
pos += len(segment) + 1 // advance past this segment and trailing slash
|
||||||
|
|
||||||
// Try static match first (fastest path)
|
// Try static match first (fastest path)
|
||||||
if child := current.findChild(segment); child != nil {
|
if child := current.findChild(segment); child != nil {
|
||||||
@@ -274,7 +305,7 @@ func (n *node) find(segments []string, params *Params) *node {
|
|||||||
|
|
||||||
// Try parameter match
|
// Try parameter match
|
||||||
if current.paramChild != nil {
|
if current.paramChild != nil {
|
||||||
if params != nil && params.keys != nil {
|
if params != nil {
|
||||||
params.Set(current.paramChild.paramName, segment)
|
params.Set(current.paramChild.paramName, segment)
|
||||||
}
|
}
|
||||||
if isLast && current.paramChild.nodeType == param {
|
if isLast && current.paramChild.nodeType == param {
|
||||||
@@ -286,30 +317,9 @@ func (n *node) find(segments []string, params *Params) *node {
|
|||||||
|
|
||||||
// Try wildcard match (catches everything)
|
// Try wildcard match (catches everything)
|
||||||
if current.wildcardChild != nil {
|
if current.wildcardChild != nil {
|
||||||
if params != nil && params.keys != nil {
|
if params != nil {
|
||||||
// Wildcard captures the rest of the path
|
// Wildcard captures the rest of the path directly from the original string
|
||||||
// Use pooled string builder for efficient concatenation
|
params.Set(current.wildcardChild.paramName, path[segmentStart:])
|
||||||
// Note: We can't use the pool here since we don't have access to router
|
|
||||||
// But we can optimize the string concatenation
|
|
||||||
var wildcardValue string
|
|
||||||
if i < len(segments)-1 {
|
|
||||||
// Pre-calculate capacity to avoid reallocations
|
|
||||||
capacity := len(segment)
|
|
||||||
for j := i + 1; j < len(segments); j++ {
|
|
||||||
capacity += 1 + len(segments[j]) // 1 for '/'
|
|
||||||
}
|
|
||||||
builder := strings.Builder{}
|
|
||||||
builder.Grow(capacity)
|
|
||||||
builder.WriteString(segment)
|
|
||||||
for j := i + 1; j < len(segments); j++ {
|
|
||||||
builder.WriteByte('/')
|
|
||||||
builder.WriteString(segments[j])
|
|
||||||
}
|
|
||||||
wildcardValue = builder.String()
|
|
||||||
} else {
|
|
||||||
wildcardValue = segment
|
|
||||||
}
|
|
||||||
params.Set(current.wildcardChild.paramName, wildcardValue)
|
|
||||||
}
|
}
|
||||||
return current.wildcardChild
|
return current.wildcardChild
|
||||||
}
|
}
|
||||||
@@ -326,63 +336,88 @@ func (n *node) find(segments []string, params *Params) *node {
|
|||||||
|
|
||||||
// ServeHTTP implements the http.Handler interface
|
// ServeHTTP implements the http.Handler interface
|
||||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
if tree, exists := r.trees[req.Method]; exists {
|
segments := r.segmentsPool.Get().([]string)
|
||||||
segments := parsePathZeroAlloc(req.URL.Path)
|
segments = segments[:0]
|
||||||
|
segments = parsePathInto(req.URL.Path, segments)
|
||||||
|
defer func() {
|
||||||
|
// Clear to allow GC of prior path strings before pooling
|
||||||
|
for i := range segments {
|
||||||
|
segments[i] = ""
|
||||||
|
}
|
||||||
|
r.segmentsPool.Put(segments[:0])
|
||||||
|
}()
|
||||||
|
|
||||||
// Get params from pool for better performance
|
tree, exists := r.trees[req.Method]
|
||||||
params := r.paramsPool.Get().(Params)
|
if !exists {
|
||||||
|
if r.checkMethodNotAllowed && r.hasOtherMethodMatch(req.URL.Path, segments, req.Method) {
|
||||||
|
r.handleMethodNotAllowed(w, req)
|
||||||
|
} else {
|
||||||
|
r.handleNotFound(w, req)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := r.paramsPool.Get().(*Params)
|
||||||
|
params.Reset()
|
||||||
|
|
||||||
|
node := tree.find(req.URL.Path, segments, params)
|
||||||
|
if node == nil || node.handler == nil {
|
||||||
params.Reset()
|
params.Reset()
|
||||||
|
r.paramsPool.Put(params)
|
||||||
|
|
||||||
node := tree.find(segments, ¶ms)
|
if r.checkMethodNotAllowed {
|
||||||
|
|
||||||
if node == nil || node.handler == nil {
|
|
||||||
// Check if the path exists for other methods to return 405 instead of 404
|
// Check if the path exists for other methods to return 405 instead of 404
|
||||||
hasOtherMethod := false
|
if r.hasOtherMethodMatch(req.URL.Path, segments, req.Method) {
|
||||||
for method, otherTree := range r.trees {
|
|
||||||
if method != req.Method {
|
|
||||||
if otherNode := otherTree.find(segments, &Params{}); otherNode != nil {
|
|
||||||
hasOtherMethod = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return params to pool
|
|
||||||
r.paramsPool.Put(params)
|
|
||||||
|
|
||||||
if hasOtherMethod {
|
|
||||||
r.handleMethodNotAllowed(w, req)
|
r.handleMethodNotAllowed(w, req)
|
||||||
} else {
|
} else {
|
||||||
r.handleNotFound(w, req)
|
r.handleNotFound(w, req)
|
||||||
}
|
}
|
||||||
return
|
} else {
|
||||||
|
r.handleNotFound(w, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add params to request context
|
|
||||||
ctx := context.WithValue(req.Context(), paramsKey, params)
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
// Create handler chain with middleware
|
|
||||||
handler := http.Handler(http.HandlerFunc(node.handler))
|
|
||||||
|
|
||||||
// Apply node-specific middleware first, then router middleware
|
|
||||||
for i := len(node.middleware) - 1; i >= 0; i-- {
|
|
||||||
handler = node.middleware[i](handler)
|
|
||||||
}
|
|
||||||
for i := len(r.middleware) - 1; i >= 0; i-- {
|
|
||||||
handler = r.middleware[i](handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defer returning params to pool after handler completes
|
|
||||||
defer func() {
|
|
||||||
r.paramsPool.Put(params)
|
|
||||||
}()
|
|
||||||
|
|
||||||
handler.ServeHTTP(w, req)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.handleMethodNotAllowed(w, req)
|
// Add params to request context as a copy to keep values stable after pooling
|
||||||
|
var ctxParams Params
|
||||||
|
if params.Len() > 0 {
|
||||||
|
ctxParams.keys = append(ctxParams.keys, params.keys...)
|
||||||
|
ctxParams.values = append(ctxParams.values, params.values...)
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), paramsKey, ctxParams)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
// Create handler chain with middleware
|
||||||
|
handler := http.Handler(http.HandlerFunc(node.handler))
|
||||||
|
|
||||||
|
// Apply node-specific middleware first, then router middleware
|
||||||
|
for i := len(node.middleware) - 1; i >= 0; i-- {
|
||||||
|
handler = node.middleware[i](handler)
|
||||||
|
}
|
||||||
|
for i := len(r.middleware) - 1; i >= 0; i-- {
|
||||||
|
handler = r.middleware[i](handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer returning params to pool after handler completes
|
||||||
|
defer func() {
|
||||||
|
params.Reset()
|
||||||
|
r.paramsPool.Put(params)
|
||||||
|
}()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasOtherMethodMatch checks whether the same path exists on another method tree.
|
||||||
|
func (r *Router) hasOtherMethodMatch(path string, segments []string, currentMethod string) bool {
|
||||||
|
for method, otherTree := range r.trees {
|
||||||
|
if method == currentMethod {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if otherNode := otherTree.find(path, segments, nil); otherNode != nil && otherNode.handler != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleNotFound handles 404 responses
|
// handleNotFound handles 404 responses
|
||||||
@@ -421,10 +456,10 @@ func (r *Router) Group(prefix string, middleware ...MiddlewareFunc) *Router {
|
|||||||
middleware: append(r.middleware, middleware...),
|
middleware: append(r.middleware, middleware...),
|
||||||
notFoundHandler: r.notFoundHandler,
|
notFoundHandler: r.notFoundHandler,
|
||||||
methodNotAllowedHandler: r.methodNotAllowedHandler,
|
methodNotAllowedHandler: r.methodNotAllowedHandler,
|
||||||
paramsPool: r.paramsPool, // Already a pointer, just copy the reference
|
paramsPool: r.paramsPool, // Already a pointer, just copy the reference
|
||||||
segmentsPool: r.segmentsPool, // Share the segments pool
|
segmentsPool: r.segmentsPool, // Share the segments pool
|
||||||
builderPool: r.builderPool, // Share the builder pool
|
|
||||||
prefix: r.prefix + prefix,
|
prefix: r.prefix + prefix,
|
||||||
|
checkMethodNotAllowed: r.checkMethodNotAllowed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +473,11 @@ func (r *Router) MethodNotAllowed(handler http.HandlerFunc) {
|
|||||||
r.methodNotAllowedHandler = handler
|
r.methodNotAllowedHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnableMethodNotAllowedCheck toggles costly cross-method lookup on 404
|
||||||
|
func (r *Router) EnableMethodNotAllowedCheck(enabled bool) {
|
||||||
|
r.checkMethodNotAllowed = enabled
|
||||||
|
}
|
||||||
|
|
||||||
// addRoute is a helper method to add routes for different HTTP methods
|
// addRoute is a helper method to add routes for different HTTP methods
|
||||||
func (r *Router) addRoute(method, path string, handler http.HandlerFunc) *Router {
|
func (r *Router) addRoute(method, path string, handler http.HandlerFunc) *Router {
|
||||||
if r.trees == nil {
|
if r.trees == nil {
|
||||||
@@ -502,8 +542,8 @@ func New() *Router {
|
|||||||
middleware: make([]MiddlewareFunc, 0),
|
middleware: make([]MiddlewareFunc, 0),
|
||||||
paramsPool: &sync.Pool{
|
paramsPool: &sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
return Params{
|
return &Params{
|
||||||
keys: make([]string, 0, 4), // Pre-allocate for common cases
|
keys: make([]string, 0, 4), // Pre-allocate for common cases
|
||||||
values: make([]string, 0, 4),
|
values: make([]string, 0, 4),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -513,12 +553,8 @@ func New() *Router {
|
|||||||
return make([]string, 0, 4) // Pre-allocate for common cases
|
return make([]string, 0, 4) // Pre-allocate for common cases
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
builderPool: &sync.Pool{
|
prefix: "",
|
||||||
New: func() interface{} {
|
checkMethodNotAllowed: true,
|
||||||
return &strings.Builder{}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
prefix: "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize trees for common HTTP methods
|
// Initialize trees for common HTTP methods
|
||||||
|
|||||||
Reference in New Issue
Block a user