Bon - Fast HTTP Router for Go
June 4, 2026 ยท View on GitHub
Bon - Fast HTTP Router for Go
Bon is a high-performance HTTP router for Go that uses exact-match indexing, segment tree routing, and a Patricia-style fallback for efficient route matching. It focuses on speed, simplicity, and zero external dependencies.
Table of Contents
- Features
- Quick Start
- Installation
- Route Patterns
- Middleware
- Groups and Routes
- HTTP Methods
- File Server
- Custom 404 Handler
- WebSocket, SSE, and HTTP/2 Push Support
- Examples
- Benchmarks
- API Documentation
- Performance Tips
- Requirements
- Testing
- Contributing
- License
Features
- High Performance: Static fast path and segment tree-based dynamic routing
- Zero Dependencies: Uses only Go standard library
- Middleware Support: Flexible middleware at router, group, and route levels
- Standard HTTP Compatible: Works with
http.Handlerinterface - Flexible Routing: Static, parameter (
:param), and wildcard (*) patterns - All HTTP Methods: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH, CONNECT, TRACE
- File Server: Built-in static file serving with security protections
- Context Pooling: Efficient memory usage with sync.Pool
- Thread-Safe: Lock-free reads using atomic operations
- Panic Recovery: Recovery middleware available
- WebSocket Ready: Works with standard WebSocket upgrade flows
- SSE Support: Server-Sent Events with proper flushing
- HTTP/2 Push: Supports
http.Pusherwhen the underlying server provides it
Quick Start
package main
import (
"net/http"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
)
func main() {
r := bon.NewRouter()
// Global middleware
r.Use(middleware.Recovery()) // Panic recovery
// Simple route
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, Bon!"))
})
// Route with parameter
r.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) {
userID := bon.URLParam(r, "id")
w.Write([]byte("User: " + userID))
})
http.ListenAndServe(":8080", r)
}
Installation
go get github.com/nissy/bon/v2
Route Patterns
Pattern Types and Priority
Routes are matched in the following priority order (highest to lowest):
-
Static routes - Exact path match
r.Get("/users/profile", handler) // Highest priority r.Get("/api/v1/status", handler) -
Parameter routes - Named parameter capture
r.Get("/users/:id", handler) // Captures id parameter r.Get("/posts/:category/:slug", handler) r.Get("/api/v:version/users", handler) // Captures version from v1, v2, etc. -
Wildcard routes - Catch-all or single-segment wildcard pattern
r.Get("/files/*", handler) // Matches the remaining path r.Get("/api/*", handler) r.Get("/files/*/metadata", handler) // * matches one segment before /metadata
Parameter names are read with bon.URLParam(r, "name"). Wildcard values are not exposed through URLParam.
Parameter Extraction
// Single parameter
r.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) {
userID := bon.URLParam(r, "id")
// Use userID...
})
// Multiple parameters
r.Get("/posts/:category/:id", func(w http.ResponseWriter, r *http.Request) {
category := bon.URLParam(r, "category")
postID := bon.URLParam(r, "id")
// Use parameters...
})
// Unicode parameter names are supported
r.Get("/users/:name", func(w http.ResponseWriter, r *http.Request) {
name := bon.URLParam(r, "name")
// Use name...
})
Middleware
Middleware Execution Order
Middleware executes in the order it was added, creating a chain:
r := bon.NewRouter()
// Execution order: Recovery -> CORS -> Auth -> Handler
r.Use(middleware.Recovery()) // 1st - Catches panics
r.Use(middleware.CORS(config)) // 2nd - Handles CORS
api := r.Group("/api")
api.Use(middleware.BasicAuth(users)) // 3rd - Authenticates
api.Get("/data", handler) // Finally, the handler
Built-in Middleware
Recovery Middleware
Catches panics and returns 500 Internal Server Error:
r.Use(middleware.Recovery())
// With custom handler
r.Use(middleware.RecoveryWithHandler(func(w http.ResponseWriter, r *http.Request, err interface{}) {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("Panic: %v", err)))
}))
CORS Middleware
Handles Cross-Origin Resource Sharing:
r.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
AllowCredentials: true,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowHeaders: []string{"Authorization", "Content-Type"},
ExposeHeaders: []string{"X-Total-Count"},
MaxAge: 86400,
}))
Basic Auth Middleware
HTTP Basic Authentication:
users := []middleware.BasicAuthUser{
{Name: "admin", Password: "secret"},
{Name: "user", Password: "pass123"},
}
r.Use(middleware.BasicAuth(users))
Timeout Middleware
Request timeout handling:
r.Use(middleware.Timeout(30 * time.Second))
Groups and Routes
Group - Prefix and Group Middleware
Groups prefix all routes and inherit middleware from parent groups. Global middleware registered with r.Use(...) is applied to every route at the router level.
r := bon.NewRouter()
r.Use(middleware.Recovery()) // Global middleware applied to every route
api := r.Group("/api")
api.Use(middleware.BasicAuth(users)) // Group middleware
// These routes use global Recovery + group BasicAuth
api.Get("/users", listUsers) // GET /api/users
api.Post("/users", createUser) // POST /api/users
// Nested group inherits parent group middleware
v1 := api.Group("/v1")
v1.Get("/posts", listPosts) // GET /api/v1/posts
Route - Standalone From Group Middleware
Routes created with Route() do not inherit group middleware. Global middleware registered with r.Use(...) still applies.
r := bon.NewRouter()
r.Use(middleware.Recovery()) // Global middleware still applies
api := r.Group("/api")
api.Use(middleware.BasicAuth(users))
api.Get("/private", handler) // Uses Recovery + BasicAuth
// This route keeps the /api prefix but does not inherit BasicAuth
public := api.Route()
public.Get("/public", handler) // Uses Recovery only
// Must explicitly add middleware if needed
webhook := api.Route()
webhook.Use(webhookMiddleware)
webhook.Post("/webhook", handler) // Uses Recovery + webhookMiddleware
HTTP Methods
All standard HTTP methods are supported:
r.Get("/users", handler)
r.Post("/users", handler)
r.Put("/users/:id", handler)
r.Delete("/users/:id", handler)
r.Head("/", handler)
r.Options("/", handler)
r.Patch("/users/:id", handler)
r.Connect("/proxy", handler)
r.Trace("/debug", handler)
// Generic method handler
r.Handle("CUSTOM", "/", handler)
File Server
Serve static files with built-in security:
// Serve files from ./public directory at /static/*
r.FileServer("/static", "./public")
// With middleware
r.FileServer("/assets", "./assets",
middleware.BasicAuth(users),
middleware.CORS(corsConfig),
)
// In a group
admin := r.Group("/admin")
admin.Use(middleware.BasicAuth(adminUsers))
admin.FileServer("/files", "./admin-files")
Security features:
- Path traversal protection (blocks
..,./, etc.) - Hidden file protection (blocks
.prefix files) - Null byte protection
- Automatic index.html serving for directories
Custom 404 Handler
r := bon.NewRouter()
// SetNotFound rebuilds the internal middleware chain for 404 responses.
r.SetNotFound(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(404)
w.Write([]byte(`{"error":"not found"}`))
})
WebSocket, SSE, and HTTP/2 Push Support
Bon works with WebSocket, Server-Sent Events (SSE), and HTTP/2 Push through Go's standard interfaces. When using middleware that wraps the ResponseWriter (like the Timeout middleware), access the underlying ResponseWriter through the Unwrap() method when needed.
WebSocket Support
package main
import (
"net/http"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Configure appropriately for production
},
}
func main() {
r := bon.NewRouter()
r.Use(middleware.Recovery())
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
// When using middleware that wraps ResponseWriter
var conn *websocket.Conn
var err error
// Try direct upgrade first
conn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
// If failed, try through Unwrap
if unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
conn, err = upgrader.Upgrade(unwrapper.Unwrap(), r, nil)
}
if err != nil {
http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest)
return
}
}
defer conn.Close()
// Handle WebSocket connection
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
break
}
if err := conn.WriteMessage(messageType, p); err != nil {
break
}
}
})
http.ListenAndServe(":8080", r)
}
Server-Sent Events (SSE) Support
package main
import (
"fmt"
"net/http"
"time"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
)
func main() {
r := bon.NewRouter()
r.Use(middleware.Recovery())
r.Use(middleware.Timeout(30 * time.Second))
r.Get("/events", func(w http.ResponseWriter, r *http.Request) {
// Set SSE headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// Get flusher
var flusher http.Flusher
var ok bool
// Try direct cast first
flusher, ok = w.(http.Flusher)
if !ok {
// Try through Unwrap
if unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
flusher, ok = unwrapper.Unwrap().(http.Flusher)
}
if !ok {
http.Error(w, "SSE not supported", http.StatusInternalServerError)
return
}
}
// Send events
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-r.Context().Done():
return
case t := <-ticker.C:
fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
flusher.Flush()
}
}
})
http.ListenAndServe(":8080", r)
}
HTTP/2 Push Support
package main
import (
"net/http"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
)
func main() {
r := bon.NewRouter()
r.Use(middleware.Recovery())
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
// Get pusher
var pusher http.Pusher
var ok bool
// Try direct cast first
pusher, ok = w.(http.Pusher)
if !ok {
// Try through Unwrap
if unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
pusher, ok = unwrapper.Unwrap().(http.Pusher)
}
}
// Push resources if available
if pusher != nil {
// Push CSS and JS files
pusher.Push("/static/style.css", &http.PushOptions{
Header: http.Header{
"Content-Type": []string{"text/css"},
},
})
pusher.Push("/static/app.js", &http.PushOptions{
Header: http.Header{
"Content-Type": []string{"application/javascript"},
},
})
}
// Serve main content
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/style.css">
<script src="/static/app.js"></script>
</head>
<body>
<h1>Hello with HTTP/2 Push!</h1>
</body>
</html>
`))
})
// Serve static files
r.FileServer("/static", "./static")
// Note: HTTP/2 requires TLS
http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", r)
}
Using http.ResponseController (Go 1.20+)
For Go 1.20 and later, you can use http.ResponseController which automatically handles the Unwrap() method:
func sseHandler(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
for {
select {
case <-r.Context().Done():
return
case <-time.After(1 * time.Second):
fmt.Fprintf(w, "data: ping\n\n")
if err := rc.Flush(); err != nil {
return
}
}
}
}
Examples
RESTful API
package main
import (
"encoding/json"
"net/http"
"time"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
func main() {
r := bon.NewRouter()
// Global middleware
r.Use(middleware.Recovery())
r.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
}))
// API routes
api := r.Group("/api")
api.Use(middleware.Timeout(30 * time.Second))
// User routes
api.Get("/users", listUsers)
api.Post("/users", createUser)
api.Get("/users/:id", getUser)
api.Put("/users/:id", updateUser)
api.Delete("/users/:id", deleteUser)
// Nested resources
api.Get("/users/:userId/posts", getUserPosts)
api.Post("/users/:userId/posts", createUserPost)
http.ListenAndServe(":8080", r)
}
func getUser(w http.ResponseWriter, r *http.Request) {
userID := bon.URLParam(r, "id")
user := User{ID: userID, Name: "John Doe"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
API Versioning
package main
import (
"net/http"
"time"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
)
func main() {
r := bon.NewRouter()
// API v1
v1 := r.Group("/api/v1")
v1.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
}))
v1.Get("/users", v1ListUsers)
v1.Get("/posts", v1ListPosts)
// API v2 with additional features
v2 := r.Group("/api/v2")
v2.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
}))
v2.Use(middleware.Timeout(30 * time.Second))
v2.Get("/users", v2ListUsers) // New response format
v2.Get("/posts", v2ListPosts) // Additional fields
v2.Get("/comments", v2ListComments) // New endpoint
// Health check (version independent)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
http.ListenAndServe(":8080", r)
}
Authentication Example
package main
import (
"net/http"
"github.com/nissy/bon/v2"
"github.com/nissy/bon/v2/middleware"
)
func main() {
r := bon.NewRouter()
// Public endpoints
r.Get("/", homeHandler)
r.Get("/login", loginPageHandler)
r.Post("/login", loginHandler)
// Protected API
api := r.Group("/api")
api.Use(middleware.BasicAuth([]middleware.BasicAuthUser{
{Name: "user", Password: "pass"},
}))
api.Get("/profile", profileHandler)
api.Get("/settings", settingsHandler)
// Admin area with different auth
admin := r.Group("/admin")
admin.Use(middleware.BasicAuth([]middleware.BasicAuthUser{
{Name: "admin", Password: "admin123"},
}))
admin.Get("/users", listAllUsers)
admin.Delete("/users/:id", deleteUser)
// Webhooks - no auth but standalone
webhooks := r.Route()
webhooks.Post("/webhook/github", githubWebhook)
webhooks.Post("/webhook/stripe", stripeWebhook)
http.ListenAndServe(":8080", r)
}
Benchmarks
Run benchmarks locally with:
go test -bench=. ./...
API Documentation
For detailed API documentation, see pkg.go.dev/github.com/nissy/bon/v2.
Performance Tips
- Route Registration: Static routes are fastest; route priority is static, then parameters, then wildcards
- Middleware Placement: Apply at the appropriate level for best performance
- Static Routes: Use exact paths when possible for fastest matching
- Parameter Reuse: The router pools context objects automatically
Requirements
- Go 1.23.6 or higher
Testing
# Run all tests
go test ./...
# Run tests with race detection
go test -race ./...
# Run benchmarks
go test -bench=. ./...
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT