Implementing an Advanced HTTP Router in Go Using Trie Data Structures
As applications grow in complexity, efficient request routing becomes essential. Go’s standard library provides a basic router with http.ServeMux, but building your own HTTP router unlocks powerful capabilities like method-based routing, path parameters, and middleware chains. This comprehensive guide walks through building an advanced HTTP router using Trie data structures.
Implementing an Advanced HTTP Router in Go Using Trie Data Structures
Understanding HTTP Routing and Its Challenges
HTTP routers are fundamental components in web applications, responsible for directing incoming requests to appropriate handlers. A router parses the request’s URL path and HTTP method, matching it against a set of registered routes to determine which handler should process the request.
Go’s standard library provides a basic HTTP router with http.ServeMux, but it has several limitations:
- No HTTP Method Routing: Cannot route based on HTTP methods (GET, POST, etc.)
- No Path Parameters: Cannot extract variables from URL paths (e.g.,
/users/:id) - No Pattern Matching: Limited pattern matching capabilities
- No Middleware Support: No built-in concept of middleware chains
Building a custom router addresses these limitations and offers a deeper understanding of HTTP handling in Go.
Why Use Trie Data Structures for Routing
Trie (prefix tree) data structures are particularly well-suited for HTTP routing for several reasons:
- Path-Based Organization: URLs naturally form a hierarchical structure that maps cleanly to a trie
- Efficient Prefix Matching: Tries excel at matching common prefixes
- Fast Lookup: O(m) lookup time, where m is the path length, not dependent on the number of routes
- Compact Representation: Can efficiently store routes with common prefixes
Let’s visualize the trie structure for a set of routes:
Routes:
GET /
GET /users
POST /users
GET /users/:id
PUT /users/:id
GET /posts
GET /posts/:id
GET /posts/:id/comments
This would create a trie structure like:
[root]
/ \
/ \
[users] [posts]
/ \ / \
/ \ / \
[<empty>] [:id] [<empty>] [:id]
GET/POST / \ GET / \
/ \ / \
GET PUT [<empty>] [comments]
GET GET
Note that each node in the trie represents a path segment, and each node can have associated HTTP method handlers.
Design Considerations for Our Router
Before diving into implementation, let’s define our requirements:
- Method-Based Routing: Support different handlers for different HTTP methods
- Path Parameters: Extract variables from URL paths (e.g.,
/users/:id) - Wildcards: Support wildcard matching (e.g.,
/static/*) - Middleware Support: Allow middleware chains before handler execution
- Route Groups: Support grouping routes with common prefixes
- Conflict Detection: Provide clear errors when conflicting routes are defined
Router Interface
The router will expose a clean API for registering routes:
// Create a new router
router := httprouter.New()
// Register simple routes
router.GET("/", indexHandler)
router.POST("/users", createUserHandler)
// Route with path parameter
router.GET("/users/:id", getUserHandler)
// Route with middleware
router.GET("/admin", authMiddleware, adminHandler)
// Route group
api := router.Group("/api")
api.GET("/users", listUsersHandler)
Let’s start implementing our router step-by-step.
Basic Implementation: Core Trie Structure
First, let’s define the basic trie structure for our router:
package httprouter
import (
"errors"
"net/http"
"strings"
)
const (
// Special node types
nodeTypeStatic = iota
nodeTypeParam
nodeTypeWildcard
)
// Constants for path handling
const (
PathRoot = "/"
PathDelimiter = "/"
)
// Common errors
var (
ErrNotFound = errors.New("not found")
ErrMethodNotAllowed = errors.New("method not allowed")
)
// Node represents a node in the trie
type Node struct {
// nodeType defines the type of node (static, parameter, wildcard)
nodeType int
// path represents the path segment
path string
// children contains the child nodes indexed by their first character
// for fast lookup
children map[string]*Node
// handlers contains handlers for different HTTP methods
handlers map[string]http.Handler
// wildcard child node, if present
wildcard *Node
// param child node, if present
param *Node
}
// Trie represents the router's trie structure
type Trie struct {
root *Node
}
// NewTrie creates a new trie for routing
func NewTrie() *Trie {
return &Trie{
root: &Node{
nodeType: nodeTypeStatic,
path: PathRoot,
children: make(map[string]*Node),
handlers: make(map[string]http.Handler),
},
}
}
Our trie structure has three types of nodes:
- Static nodes: Match exact path segments
- Parameter nodes: Match any segment and extract it as a parameter (e.g.,
:id) - Wildcard nodes: Match any remaining part of the path (e.g.,
*or*filepath)
Adding Routes to the Trie
Now let’s implement the function to add routes to our trie:
// Insert adds a route to the trie
func (t *Trie) Insert(method, path string, handler http.Handler) error {
// Ensure path starts with /
if !strings.HasPrefix(path, PathRoot) {
path = PathRoot + path
}
// Handle root path separately
if path == PathRoot {
t.root.handlers[method] = handler
return nil
}
// Split path into segments
segments := splitPath(path)
// Start from the root node
current := t.root
// Process each path segment
for i, segment := range segments {
// Check if this is a parameter segment (starts with :)
if strings.HasPrefix(segment, ":") {
paramName := segment[1:]
// Create parameter node if it doesn't exist
if current.param == nil {
current.param = &Node{
nodeType: nodeTypeParam,
path: paramName,
children: make(map[string]*Node),
handlers: make(map[string]http.Handler),
}
}
current = current.param
continue
}
// Check if this is a wildcard segment
if segment == "*" || strings.HasPrefix(segment, "*") {
wildcardName := ""
if segment != "*" {
wildcardName = segment[1:]
}
// Create wildcard node if it doesn't exist
if current.wildcard == nil {
current.wildcard = &Node{
nodeType: nodeTypeWildcard,
path: wildcardName,
children: make(map[string]*Node),
handlers: make(map[string]http.Handler),
}
}
// Wildcard must be the last segment
if i != len(segments)-1 {
return errors.New("wildcard must be the last segment in the path")
}
current = current.wildcard
break
}
// Handle static segments
child, exists := current.children[segment]
if !exists {
// Create a new node for this segment
child = &Node{
nodeType: nodeTypeStatic,
path: segment,
children: make(map[string]*Node),
handlers: make(map[string]http.Handler),
}
current.children[segment] = child
}
current = child
}
// Register the handler for the specified HTTP method
current.handlers[method] = handler
return nil
}
// Helper function to split a path into segments
func splitPath(path string) []string {
segments := strings.Split(path, PathDelimiter)
var result []string
for _, segment := range segments {
if segment != "" {
result = append(result, segment)
}
}
return result
}
Searching the Trie
Now we need to implement the search function to find the appropriate handler for a given path:
// RouteMatch represents the result of a successful route match
type RouteMatch struct {
Handler http.Handler
Params map[string]string
MatchedURL string
}
// Search finds a handler in the trie matching the given method and path
func (t *Trie) Search(method, path string) (*RouteMatch, error) {
// Ensure path starts with /
if !strings.HasPrefix(path, PathRoot) {
path = PathRoot + path
}
// Handle root path separately
if path == PathRoot {
handler, exists := t.root.handlers[method]
if !exists {
return nil, ErrMethodNotAllowed
}
return &RouteMatch{
Handler: handler,
Params: make(map[string]string),
MatchedURL: PathRoot,
}, nil
}
// Split path into segments
segments := splitPath(path)
// Initialize result for collecting path parameters
params := make(map[string]string)
// Start search from the root
match, found := searchNode(t.root, segments, method, params, 0)
if !found {
return nil, ErrNotFound
}
if match.Handler == nil {
return nil, ErrMethodNotAllowed
}
return match, nil
}
// searchNode recursively searches for a matching node in the trie
func searchNode(node *Node, segments []string, method string, params map[string]string, index int) (*RouteMatch, bool) {
// If we've processed all segments, check if this node has a handler for the requested method
if index >= len(segments) {
handler, exists := node.handlers[method]
if !exists {
// We found the path but not for this method
return &RouteMatch{
Handler: nil,
Params: params,
MatchedURL: "",
}, true
}
return &RouteMatch{
Handler: handler,
Params: params,
MatchedURL: "",
}, true
}
segment := segments[index]
// Try exact match first (priority order: exact > param > wildcard)
if child, exists := node.children[segment]; exists {
if match, found := searchNode(child, segments, method, params, index+1); found {
return match, true
}
}
// Try parameter match
if node.param != nil {
// Clone params to avoid modifying the original on backtracking
paramsCopy := copyParams(params)
paramsCopy[node.param.path] = segment
if match, found := searchNode(node.param, segments, method, paramsCopy, index+1); found {
return match, true
}
}
// Try wildcard match (must be at the end)
if node.wildcard != nil {
// For wildcards, capture all remaining segments
remaining := strings.Join(segments[index:], PathDelimiter)
// Clone params to avoid modifying the original
paramsCopy := copyParams(params)
if node.wildcard.path != "" {
// If wildcard has a name (e.g., *filepath), capture the value
paramsCopy[node.wildcard.path] = remaining
}
// Check if wildcard node has the requested method
handler, exists := node.wildcard.handlers[method]
if !exists {
return &RouteMatch{
Handler: nil,
Params: paramsCopy,
MatchedURL: "",
}, true
}
return &RouteMatch{
Handler: handler,
Params: paramsCopy,
MatchedURL: "",
}, true
}
// No match found
return nil, false
}
// Helper function to copy path parameters map
func copyParams(params map[string]string) map[string]string {
copy := make(map[string]string, len(params))
for k, v := range params {
copy[k] = v
}
return copy
}
Router Implementation with Method-Based Support
Now that we have our trie data structure, let’s implement the actual router:
// Router is the HTTP router
type Router struct {
trie *Trie
notFound http.Handler
methodNotAllowed http.Handler
paramsKey interface{}
}
// New creates a new Router
func New() *Router {
return &Router{
trie: NewTrie(),
notFound: http.NotFoundHandler(),
methodNotAllowed: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}),
paramsKey: contextKey("params"),
}
}
// context key type to avoid collisions
type contextKey string
// ServeHTTP implements the http.Handler interface
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Find the handler for this path
match, err := r.trie.Search(req.Method, req.URL.Path)
if err != nil {
switch err {
case ErrNotFound:
r.notFound.ServeHTTP(w, req)
case ErrMethodNotAllowed:
r.methodNotAllowed.ServeHTTP(w, req)
default:
// Unexpected error, respond with internal server error
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}
// If we have path parameters, add them to the request context
if len(match.Params) > 0 {
ctx := context.WithValue(req.Context(), r.paramsKey, match.Params)
req = req.WithContext(ctx)
}
// Call the handler
match.Handler.ServeHTTP(w, req)
}
// GET registers a route for GET requests
func (r *Router) GET(path string, handler http.HandlerFunc) {
r.Handle(http.MethodGet, path, handler)
}
// POST registers a route for POST requests
func (r *Router) POST(path string, handler http.HandlerFunc) {
r.Handle(http.MethodPost, path, handler)
}
// PUT registers a route for PUT requests
func (r *Router) PUT(path string, handler http.HandlerFunc) {
r.Handle(http.MethodPut, path, handler)
}
// DELETE registers a route for DELETE requests
func (r *Router) DELETE(path string, handler http.HandlerFunc) {
r.Handle(http.MethodDelete, path, handler)
}
// PATCH registers a route for PATCH requests
func (r *Router) PATCH(path string, handler http.HandlerFunc) {
r.Handle(http.MethodPatch, path, handler)
}
// Handle registers a route with a method, path and handler
func (r *Router) Handle(method, path string, handler http.Handler) {
err := r.trie.Insert(method, path, handler)
if err != nil {
panic(err)
}
}
// NotFound sets the handler for 404 responses
func (r *Router) NotFound(handler http.Handler) {
r.notFound = handler
}
// MethodNotAllowed sets the handler for 405 responses
func (r *Router) MethodNotAllowed(handler http.Handler) {
r.methodNotAllowed = handler
}
// Params returns the path parameters from the request context
func Params(r *http.Request) map[string]string {
// Check if we have any params
if r == nil || r.Context() == nil {
return make(map[string]string)
}
// Try to extract params from context
if params, ok := r.Context().Value(contextKey("params")).(map[string]string); ok {
return params
}
return make(map[string]string)
}
// Param returns a specific path parameter value
func Param(r *http.Request, name string) string {
return Params(r)[name]
}
This implementation provides method-based routing with a clean, intuitive API.
Adding Middleware Support
Now let’s enhance our router with middleware support:
// Middleware represents a handler middleware
type Middleware func(http.Handler) http.Handler
// Router with middleware support
type Router struct {
trie *Trie
notFound http.Handler
methodNotAllowed http.Handler
paramsKey interface{}
middleware []Middleware // Global middleware
}
// Use adds middleware to the router
func (r *Router) Use(middleware ...Middleware) {
r.middleware = append(r.middleware, middleware...)
}
// applyMiddleware wraps a handler with all registered middleware
func (r *Router) applyMiddleware(handler http.Handler) http.Handler {
// Apply middleware in reverse order (last added, first executed)
for i := len(r.middleware) - 1; i >= 0; i-- {
handler = r.middleware[i](handler)
}
return handler
}
// ServeHTTP with middleware support
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Find the handler for this path
match, err := r.trie.Search(req.Method, req.URL.Path)
if err != nil {
switch err {
case ErrNotFound:
r.notFound.ServeHTTP(w, req)
case ErrMethodNotAllowed:
r.methodNotAllowed.ServeHTTP(w, req)
default:
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}
// If we have path parameters, add them to the request context
if len(match.Params) > 0 {
ctx := context.WithValue(req.Context(), r.paramsKey, match.Params)
req = req.WithContext(ctx)
}
// Apply middleware to the handler
handler := r.applyMiddleware(match.Handler)
// Call the handler
handler.ServeHTTP(w, req)
}
// Handle with middleware support
func (r *Router) Handle(method, path string, handler http.Handler, middleware ...Middleware) {
// Apply route-specific middleware
for i := len(middleware) - 1; i >= 0; i-- {
handler = middleware[i](handler)
}
err := r.trie.Insert(method, path, handler)
if err != nil {
panic(err)
}
}
// GET with middleware support
func (r *Router) GET(path string, handler http.Handler, middleware ...Middleware) {
r.Handle(http.MethodGet, path, handler, middleware...)
}
// Similar implementation for POST, PUT, DELETE, etc.
Adding Route Groups
Route groups help organize routes with a common prefix and middleware:
// Group represents a group of routes
type Group struct {
router *Router
prefix string
middleware []Middleware
}
// Group creates a new route group
func (r *Router) Group(prefix string) *Group {
return &Group{
router: r,
prefix: prefix,
middleware: []Middleware{},
}
}
// Use adds middleware to the group
func (g *Group) Use(middleware ...Middleware) *Group {
g.middleware = append(g.middleware, middleware...)
return g
}
// Handle registers a route with this group
func (g *Group) Handle(method, path string, handler http.Handler, middleware ...Middleware) {
// Combine group middleware with route middleware
allMiddleware := append(g.middleware, middleware...)
// Apply the middleware
for i := len(allMiddleware) - 1; i >= 0; i-- {
handler = allMiddleware[i](handler)
}
// Register with the router using the full path
fullPath := g.prefix + path
g.router.Handle(method, fullPath, handler)
}
// GET registers a GET route with this group
func (g *Group) GET(path string, handler http.Handler, middleware ...Middleware) {
g.Handle(http.MethodGet, path, handler, middleware...)
}
// Similar implementation for POST, PUT, DELETE, etc.
// Group creates a sub-group
func (g *Group) Group(prefix string) *Group {
return &Group{
router: g.router,
prefix: g.prefix + prefix,
middleware: append([]Middleware{}, g.middleware...),
}
}
Performance Optimizations
Our router is functional, but we can make it more efficient with some optimizations:
1. Radix Tree Compression
We can compress the trie into a radix tree by merging nodes with a single child:
// compress merges nodes with just one static child
func (n *Node) compress() {
// Compress children first (depth-first)
for _, child := range n.children {
child.compress()
}
// If we have a parameter or wildcard child, compress it too
if n.param != nil {
n.param.compress()
}
if n.wildcard != nil {
n.wildcard.compress()
}
// If this node has exactly one static child and no handlers or other child types, merge them
if len(n.children) == 1 && len(n.handlers) == 0 && n.param == nil && n.wildcard == nil {
// Get the single child
var childKey string
var childNode *Node
for k, v := range n.children {
childKey = k
childNode = v
break
}
// If the child also has no handlers and no special children, merge
if childNode.nodeType == nodeTypeStatic && len(childNode.handlers) == 0 &&
childNode.param == nil && childNode.wildcard == nil {
// Merge path segments
n.path = n.path + PathDelimiter + childNode.path
// Adopt grandchildren
n.children = childNode.children
// Clear the now-merged child
delete(n.children, childKey)
}
}
}
2. Sorted Child Matching
For static nodes with many children, we can optimize the search by sorting children by frequency:
// childrenByFrequency helps prioritize matching based on access frequency
type childFrequency struct {
path string
node *Node
frequency int64
}
// updateMatchFrequency increments the match frequency counter
func (n *Node) updateMatchFrequency(segment string) {
if n.childrenFrequency == nil {
n.childrenFrequency = make(map[string]*childFrequency)
}
freq, exists := n.childrenFrequency[segment]
if !exists {
freq = &childFrequency{
path: segment,
node: n.children[segment],
frequency: 0,
}
n.childrenFrequency[segment] = freq
}
freq.frequency++
// Reorder children by frequency for faster matching of common paths
if len(n.childrenFrequency) > 1 && freq.frequency % 100 == 0 {
n.sortChildrenByFrequency()
}
}
// sortChildrenByFrequency sorts children by access frequency
func (n *Node) sortChildrenByFrequency() {
// Implementation omitted for brevity
}
3. Path Segment Caching
Cache path segments to avoid repeated string splitting:
// pathSegmentCache caches split path segments
var pathSegmentCache = &sync.Map{}
// splitPathCached splits a path into segments with caching
func splitPathCached(path string) []string {
// Check cache first
if cached, ok := pathSegmentCache.Load(path); ok {
return cached.([]string)
}
// Split and cache the result
segments := splitPath(path)
pathSegmentCache.Store(path, segments)
return segments
}
Using Our Router in an Application
Let’s see how to use our advanced router in a real application:
package main
import (
"fmt"
"log"
"net/http"
"time"
"example.com/httprouter"
)
// Middleware for logging
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
// Middleware for authentication
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "valid-token" {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprint(w, "Unauthorized")
return
}
next.ServeHTTP(w, r)
})
}
func main() {
// Create a new router
router := httprouter.New()
// Add global middleware
router.Use(LoggerMiddleware)
// Basic routes
router.GET("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Welcome to the home page!")
}))
router.GET("/users/:id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := httprouter.Param(r, "id")
fmt.Fprintf(w, "User details for user: %s", id)
}))
// Route with specific middleware
router.GET("/admin", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Admin page")
}), AuthMiddleware)
// Route group for API endpoints
api := router.Group("/api")
api.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
})
// API routes
api.GET("/users", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"users": [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]}`)
}))
api.GET("/users/:id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := httprouter.Param(r, "id")
fmt.Fprintf(w, `{"id": %s, "name": "User %s"}`, id, id)
}))
// Start the server
log.Println("Server starting on port 8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
Benchmarking Our Router
It’s important to measure the performance of our router. Here’s a simple benchmark comparing it with the standard library and some popular alternatives:
package httprouter_test
import (
"net/http"
"net/http/httptest"
"testing"
"example.com/httprouter"
"github.com/go-chi/chi/v5"
"github.com/gorilla/mux"
julienschmidt "github.com/julienschmidt/httprouter"
)
func BenchmarkRouterSimple(b *testing.B) {
b.Run("StandardServeMux", func(b *testing.B) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
mux.HandleFunc("/users/123", func(w http.ResponseWriter, r *http.Request) {})
req, _ := http.NewRequest("GET", "/users/123", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
}
})
b.Run("OurRouter", func(b *testing.B) {
router := httprouter.New()
router.GET("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
router.GET("/users/:id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
req, _ := http.NewRequest("GET", "/users/123", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
}
})
// Similar benchmarks for other routers omitted for brevity
}
Typical benchmark results might look like:
| Router | Operations/sec | Allocations/op | Bytes/op |
|---|---|---|---|
| net/http.ServeMux | 6,000,000 | 0 | 0 |
| Our Router | 2,500,000 | 4 | 160 |
| gorilla/mux | 250,000 | 14 | 1,312 |
| julienschmidt/httprouter | 4,500,000 | 1 | 64 |
| go-chi/chi | 1,000,000 | 7 | 368 |
These numbers are illustrative and would vary based on route complexity, implementation details, and benchmark methodology.
Advanced Features and Enhancements
Here are some additional features we could add to make our router even more powerful:
1. Regular Expression Routing
Add support for regex-based path matching:
// RegexNode extends Node with regex matching
type RegexNode struct {
pattern *regexp.Regexp
names []string
}
// Insert with regex support
func (t *Trie) Insert(method, path string, handler http.Handler) error {
// Detect regex patterns like /users/{id:[0-9]+}
if strings.Contains(path, "{") && strings.Contains(path, "}") {
// Parse regex pattern
pattern, names := parseRegexPattern(path)
// Create regex node
// Implementation omitted for brevity
}
// Regular insertion for non-regex paths
// ...
}
2. Automatic OPTIONS Handling
Add automatic handling of OPTIONS requests:
// autoOptions automatically responds to OPTIONS requests
func (r *Router) autoOptions() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Only handle OPTIONS requests
if req.Method != http.MethodOptions {
r.methodNotAllowed.ServeHTTP(w, req)
return
}
// Find all methods allowed for this path
methods := r.getAllowedMethods(req.URL.Path)
// If no methods are allowed, return 404
if len(methods) == 0 {
r.notFound.ServeHTTP(w, req)
return
}
// Add Allow header with allowed methods
w.Header().Set("Allow", strings.Join(methods, ", "))
w.WriteHeader(http.StatusNoContent)
})
}
// getAllowedMethods returns all methods registered for a path
func (r *Router) getAllowedMethods(path string) []string {
// Implementation omitted for brevity
return []string{}
}
3. CORS Middleware
Add built-in CORS support:
// CORSConfig defines CORS configuration
type CORSConfig struct {
AllowOrigins []string
AllowMethods []string
AllowHeaders []string
AllowCredentials bool
MaxAge int
}
// CORSMiddleware creates a CORS middleware with the given config
func CORSMiddleware(config CORSConfig) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
if len(config.AllowOrigins) > 0 {
w.Header().Set("Access-Control-Allow-Origin", strings.Join(config.AllowOrigins, ", "))
}
if len(config.AllowMethods) > 0 {
w.Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowMethods, ", "))
}
if len(config.AllowHeaders) > 0 {
w.Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowHeaders, ", "))
}
if config.AllowCredentials {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if config.MaxAge > 0 {
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(config.MaxAge))
}
// Handle preflight requests
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
Best Practices for Router Implementation
When building or using an HTTP router, consider these best practices:
1. Route Organization
- Group related routes together
- Use consistent path patterns and naming conventions
- Organize routes by resource, not by HTTP method
2. Error Handling
- Provide descriptive error messages for routing conflicts
- Implement custom handlers for common error cases (404, 405)
- Log routing errors for debugging
3. Security Considerations
- Validate URL parameters to prevent injection attacks
- Implement rate limiting middleware
- Use HTTPS redirects where appropriate
4. Performance
- Benchmark your router with realistic workloads
- Use profiling to identify bottlenecks
- Consider the impact of middleware chains on performance
Conclusion
Building a custom HTTP router in Go provides valuable insights into HTTP handling, algorithm design, and performance optimization. Our implementation offers several advantages over the standard library’s router:
- Method-based routing for cleaner handler organization
- Path parameters for dynamic route segments
- Wildcards for flexible path matching
- Middleware support for cross-cutting concerns
- Route groups for logical organization
While many production applications will use established routers like chi, gorilla/mux, or echo, understanding how these routers work under the hood makes you a better Go developer. The principles covered in this guide apply to other languages and frameworks as well.
The full source code for this router is available on GitHub (fictional link for illustration).
Note: While this router is functional, production applications should consider using well-established, thoroughly tested routers for critical systems. Building your own router is primarily a learning experience or for specialized use cases.