A Go module that provides a high-performance parser based on text/template for HTTP requests. This module is designed to handle non-rewindable HTTP input streams efficiently while providing template caching and automatic recompilation on file changes.
Performance: Capable of processing 40,000+ requests per second with template caching, optimized for high-throughput web applications and microservices.
- Re-readable HTTP Requests: Handles non-rewindable HTTP input streams by buffering them in memory
- Template Caching: High-performance template caching with LRU eviction
- File Watching: Automatic template recompilation when template files change
- Flexible Template Loading: Supports file system and memory-based template loaders
- Rich Request Data: Extracts headers, query parameters, form data, and body content
- Custom Function Maps: Support for custom template functions
- Thread-Safe: Concurrent access safe across goroutines
- Benchmarked Performance: Optimized for high-throughput scenarios
go get github.com/fabricates/parser
package main
import (
"bytes"
"fmt"
"net/http"
"strings"
"github.com/fabricates/parser"
)
func main() {
// Simple configuration (uses MemoryLoader by default)
config := parser.Config{
MaxCacheSize: 100,
FuncMap: parser.DefaultFuncMap(),
}
// Create parser
p, err := parser.NewParser(config)
if err != nil {
panic(err)
}
defer p.Close()
// Add template dynamically
err = p.UpdateTemplate("greeting", "Hello {{.Request.Method}} from {{.Request.URL.Path}}!")
if err != nil {
panic(err)
}
// Create HTTP request
req, _ := http.NewRequest("GET", "http://example.com/api/users", nil)
// Parse template
var output bytes.Buffer
err = p.Parse("greeting", req, &output)
if err != nil {
panic(err)
}
fmt.Print(output.String()) // Output: Hello GET from /api/users!
}
func main() {
// Create a memory-based template loader explicitly
loader := parser.NewMemoryLoader()
loader.AddTemplate("greeting", "Hello {{.Request.Method}} from {{.Request.URL.Path}}!")
// Create parser configuration
config := parser.Config{
TemplateLoader: loader,
MaxCacheSize: 100,
WatchFiles: false,
FuncMap: parser.DefaultFuncMap(),
}
// Create parser
p, err := parser.NewParser(config)
if err != nil {
panic(err)
}
defer p.Close()
// Create HTTP request
req, _ := http.NewRequest("GET", "http://example.com/api/users", nil)
// Parse template
var output bytes.Buffer
err = p.Parse("greeting", req, &output)
if err != nil {
panic(err)
}
fmt.Print(output.String()) // Output: Hello GET from /api/users!
}
The main interface provides methods for template parsing and management:
type Parser interface {
Parse(templateName string, request *http.Request, output io.Writer) error
ParseWith(templateName string, request *http.Request, data interface{}, output io.Writer) error
UpdateTemplate(name string, content string) error
GetCacheStats() CacheStats
Close() error
}
Load templates from the file system with optional file watching:
loader := parser.NewFileSystemLoader("/path/to/templates", ".tmpl", true)
config := parser.Config{
TemplateLoader: loader,
WatchFiles: true, // Enable automatic reloading
MaxCacheSize: 50,
}
p, err := parser.NewParser(config)
For testing or when templates are embedded:
loader := parser.NewMemoryLoader()
loader.AddTemplate("welcome", "Welcome {{.Custom.username}}!")
config := parser.Config{
TemplateLoader: loader,
MaxCacheSize: 10,
}
p, err := parser.NewParser(config)
Templates have access to structured request data:
type RequestData struct {
Request *http.Request // Original HTTP request
Headers map[string][]string // HTTP headers
Query map[string][]string // Query parameters
Form map[string][]string // Form data (for POST requests)
Body string // Request body as string
Custom interface{} // Custom data passed to ParseWith
}
You can dynamically add or update templates at runtime using the UpdateTemplate
method:
// Add a new template
templateContent := "Hello {{.Request.Method}} from {{.Request.URL.Path}}!"
err := parser.UpdateTemplate("greeting", templateContent)
if err != nil {
log.Fatalf("Failed to update template: %v", err)
}
// Later, update the same template with new content
newContent := "Updated: {{.Request.Method}} {{.Request.URL.Path}}"
err = parser.UpdateTemplate("greeting", newContent)
if err != nil {
log.Fatalf("Failed to update template: %v", err)
}
// Use the updated template
var output bytes.Buffer
err = parser.Parse("greeting", request, &output)
Note: The UpdateTemplate
method automatically calculates MD5 hashes of template content for change detection and caching optimization. If you call UpdateTemplate
with the same content multiple times, the template will only be recompiled when the content actually changes.
Method: {{.Request.Method}}
URL: {{.Request.URL.Path}}
User-Agent: {{index .Headers "User-Agent" 0}}
{{if .Query.name}}
Hello {{index .Query "name" 0}}!
{{end}}
{{if .Form.username}}
Username: {{index .Form "username" 0}}
{{end}}
{{if .Body}}
Received: {{.Body}}
{{end}}
customData := map[string]interface{}{
"user_id": 123,
"role": "admin",
}
err := parser.ParseWith("template", request, customData, output)
User ID: {{.Custom.user_id}}
Role: {{.Custom.role}}
The parser includes useful template functions:
upper
: Convert string to uppercaselower
: Convert string to lowercasetitle
: Convert string to title casetrim
: Remove leading/trailing whitespacehasPrefix
: Check if string starts with prefixhasSuffix
: Check if string ends with suffixcontains
: Check if string contains substringreplace
: Replace all occurrences of old substring with newsplit
: Split string by separator into slicejoin
: Join slice of strings with separatortrimPrefix
: Remove prefix from start of stringtrimSuffix
: Remove suffix from end of stringrepeat
: Repeat string n timessubstr
: Extract substring (start, length)
default
: Provide default value for empty/nil values
header
: Get request header valuequery
: Get query parameter valueform
: Get form field value
Example usage:
Name: {{.Custom.name | upper | default "Anonymous"}}
Content-Type: {{header .Request "Content-Type"}}
URL: {{if hasPrefix .Request.URL.Path "/api"}}API Call{{end}}
File: {{trimSuffix .Custom.filename ".txt"}}
Tags: {{join .Custom.tags ", "}}
When WatchFiles
is enabled, the parser automatically detects template file changes and recompiles them:
config := parser.Config{
TemplateLoader: parser.NewFileSystemLoader("./templates", ".tmpl", true),
WatchFiles: true,
MaxCacheSize: 100,
}
p, err := parser.NewParser(config)
// Templates will be automatically reloaded when files change
Templates are cached after compilation with LRU eviction:
config := parser.Config{
TemplateLoader: loader,
MaxCacheSize: 100, // Cache up to 100 templates
}
// Get cache statistics
stats := p.GetCacheStats()
fmt.Printf("Cache: %d/%d, Hits: %d\n", stats.Size, stats.MaxSize, stats.HitCount)
HTTP request bodies are automatically buffered to allow multiple reads:
// The parser handles this automatically
rereadableReq, err := parser.NewRereadableRequest(originalRequest)
rereadableReq.Reset() // Reset body for re-reading
The module defines specific error types:
var (
ErrTemplateNotFound = errors.New("template not found")
ErrWatcherClosed = errors.New("file watcher is closed")
ErrInvalidConfig = errors.New("invalid configuration")
ErrParserClosed = errors.New("parser is closed")
)
Run the test suite:
go test -v
Run benchmarks:
go test -bench=.
Example benchmark results:
BenchmarkParserParse-2 193795 6008 ns/op
BenchmarkRequestExtraction-2 314965 3690 ns/op
See the /examples
directory for complete usage examples:
examples/basic/
: Basic usage with different request types- More examples coming soon!
type Config struct {
TemplateLoader TemplateLoader // How to load templates (defaults to MemoryLoader if nil)
WatchFiles bool // Enable file watching (FileSystemLoader only)
MaxCacheSize int // Template cache size (0 = unlimited)
FuncMap template.FuncMap // Custom template functions
}
If no TemplateLoader
is specified in the config, the parser will automatically use a MemoryLoader
by default. This allows you to create a parser with minimal configuration:
// Simple config with default MemoryLoader
config := parser.Config{
MaxCacheSize: 100,
}
p, err := parser.NewParser(config)
if err != nil {
panic(err)
}
// Add templates dynamically using UpdateTemplate
err = p.UpdateTemplate("greeting", "Hello {{.Request.Method}}!")
The parser is designed for high performance with several optimizations:
Performance characteristics on a typical server (Intel Xeon E5-2680 v2 @ 2.80GHz):
Operation | Throughput | Memory per Op | Allocations |
---|---|---|---|
Basic Parsing | ~48,000 ops/sec | 4.9 KB | 67 allocs |
Request Extraction | ~105,000 ops/sec | 4.9 KB | 41 allocs |
Generic String Output | ~50,000 ops/sec | 4.5 KB | 69 allocs |
Generic JSON Output | ~22,000 ops/sec | 6.2 KB | 104 allocs |
Template Caching | ~89,000 ops/sec | 3.7 KB | 34 allocs |
Template Updates | ~85,000 ops/sec | 3.3 KB | 43 allocs |
Re-readable Requests | ~437,000 ops/sec | 1.2 KB | 10 allocs |
Complex Templates | ~8,000 ops/sec | 14.3 KB | 268 allocs |
Template caching provides significant performance benefits:
Cache Size | Performance | Memory Efficiency |
---|---|---|
Size 1 | ~46,000 ops/sec | Most memory efficient |
Size 10 | ~50,000 ops/sec | Balanced |
Size 100 | ~42,000 ops/sec | Best hit rate |
Unlimited | ~44,000 ops/sec | Highest memory usage |
Recommendation: Use cache size 10-50 for most applications.
Request body size affects memory usage and performance:
Body Size | Throughput | Memory per Op |
---|---|---|
Small (100B) | ~45,000 ops/sec | 5.2 KB |
Medium (10KB) | ~15,000 ops/sec | 60.9 KB |
Large (100KB) | ~2,000 ops/sec | 625.4 KB |
The parser maintains good performance under concurrent load:
- Concurrent Parsing: ~40,000 ops/sec with multiple goroutines
- Thread-safe: No performance degradation with concurrent access
- Lock-free reads: Template cache uses efficient concurrent access patterns
- Use appropriate cache size: 10-50 templates for most applications
- Minimize template complexity: Simpler templates execute faster
- Reuse parser instances: Creating parsers has overhead
- Pre-load templates: Use
UpdateTemplate
to warm the cache - Monitor cache hit rates: Use
GetCacheStats()
to optimize cache size
All components are designed to be thread-safe and can be used concurrently across multiple goroutines.
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.