Skip to content

Commit 36e7fd4

Browse files
committed
feat: initial implementation of generic type-safe dispatcher with middleware support
- Adds Registry and SealedRegistry types for handler registration and dispatch - Supports type-safe generic handlers via Go generics - Includes composable Middleware and MiddlewareFunc pattern - Zero allocations on dispatch (steady state) - SealedRegistry is lock-free and concurrency-safe
1 parent 79c1524 commit 36e7fd4

10 files changed

Lines changed: 585 additions & 2 deletions

File tree

.github/workflows/go-quality.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Go Quality Checks
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
quality-checks:
11+
name: Code Quality
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Set up Go
15+
uses: actions/setup-go@v4
16+
with:
17+
go-version: 1.24
18+
cache: true
19+
20+
- name: Check out code
21+
uses: actions/checkout@v3
22+
23+
- name: Run golangci-lint
24+
uses: golangci/golangci-lint-action@v8
25+
26+
- name: Run tests
27+
run: |
28+
go test -v ./...
29+
30+
- name: Install govulncheck
31+
run: |
32+
go install golang.org/x/vuln/cmd/govulncheck@latest
33+
34+
- name: Verify go.mod is tidy
35+
run: |
36+
go mod tidy
37+
git diff --exit-code go.mod|| (echo "Please run 'go mod tidy' and commit the changes" && exit 1)
38+
39+
- name: Check formatting
40+
run: |
41+
go fmt ./...
42+
git diff --exit-code || (echo "Please run 'go fmt ./...' and commit the changes" && exit 1)
43+
44+
- name: Run go vet
45+
run: |
46+
go vet ./...
47+
48+
- name: Run govulncheck
49+
run: |
50+
govulncheck ./...
51+

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 struct0x
3+
Copyright (c) 2025 Radosław Dejnek
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,173 @@
1-
# dispatcher
1+
# Dispatcher
2+
3+
A high-performance, type-safe dispatcher for Go.
4+
5+
[![Go Reference](https://pkg.go.dev/badge/github.com/struct0x/dispatcher.svg)](https://pkg.go.dev/github.com/struct0x/dispatcher)
6+
7+
## Overview
8+
9+
Dispatcher is a Go library that provides a fast and efficient way to route values to their appropriate handlers based on type.
10+
It's designed with performance in mind,
11+
offering both thread-safe and immutable variants with different performance characteristics.
12+
13+
## Features
14+
15+
- **Type Safety**: Compile-time type safety with generic handlers
16+
- **Zero Allocations**: Dispatch operations produce zero allocations in steady state
17+
- **High Performance**: Optimized for concurrent access patterns
18+
- **Middleware Support**: Chainable middleware for cross-cutting concerns
19+
- **Two Registry Types**:
20+
- `Registry`: Thread-safe, dynamic registration
21+
- `SealedRegistry`: Immutable, zero mutex overhead
22+
23+
## Installation
24+
25+
```bash
26+
go get github.com/struct0x/dispatcher
27+
```
28+
29+
## Usage
30+
31+
### Basic Example
32+
33+
```go
34+
package main
35+
36+
import (
37+
"context"
38+
"fmt"
39+
"log"
40+
41+
"github.com/struct0x/dispatcher"
42+
)
43+
44+
type UserCreated struct {
45+
ID int
46+
Name string
47+
}
48+
49+
type OrderPlaced struct {
50+
OrderID string
51+
Amount float64
52+
}
53+
54+
func main() {
55+
// Create a new registry
56+
reg := dispatcher.NewRegistry()
57+
58+
// Register handlers for different types
59+
dispatcher.Register[UserCreated](reg, func(ctx context.Context, event UserCreated) error {
60+
fmt.Printf("User created: %s (ID: %d)\n", event.Name, event.ID)
61+
return nil
62+
})
63+
64+
dispatcher.Register[OrderPlaced](reg, func(ctx context.Context, event OrderPlaced) error {
65+
fmt.Printf("Order placed: %s for $%.2f\n", event.OrderID, event.Amount)
66+
return nil
67+
})
68+
69+
// Dispatch events
70+
ctx := context.Background()
71+
72+
if err := dispatcher.Dispatch(reg, ctx, UserCreated{ID: 1, Name: "Alice"}); err != nil {
73+
log.Fatal(err)
74+
}
75+
76+
if err := dispatcher.Dispatch(reg, ctx, OrderPlaced{OrderID: "ORD-001", Amount: 99.99}); err != nil {
77+
log.Fatal(err)
78+
}
79+
}
80+
```
81+
82+
### Using Middleware
83+
84+
```go
85+
// Define middleware
86+
loggingMiddleware := func(next dispatcher.HandlerFunc[UserCreated]) dispatcher.HandlerFunc[UserCreated] {
87+
return func(ctx context.Context, event UserCreated) error {
88+
fmt.Printf("Processing user: %s\n", event.Name)
89+
err := next(ctx, event)
90+
fmt.Printf("Finished processing user: %s\n", event.Name)
91+
return err
92+
}
93+
}
94+
95+
// Register with middleware
96+
dispatcher.Register[UserCreated](reg, handler, loggingMiddleware)
97+
```
98+
99+
### Sealed Registry for Maximum Performance
100+
101+
```go
102+
// After registering all handlers, seal the registry for better performance
103+
sealedReg := reg.Seal()
104+
105+
// SealedRegistry has zero mutex overhead
106+
if err := dispatcher.Dispatch(sealedReg, ctx, UserCreated{ID: 2, Name: "Bob"}); err != nil {
107+
log.Fatal(err)
108+
}
109+
```
110+
111+
## Performance
112+
113+
Dispatcher is optimized for high-performance scenarios:
114+
115+
- **Zero Allocations**: Dispatch operations after initialization produce zero allocations
116+
- **Concurrent Safe**: `Registry` can be used safely across goroutines
117+
- **Sealed Optimization**: `SealedRegistry` eliminates all mutex overhead
118+
- **Benchmark Results** (Apple M2 Max):
119+
- `Registry`:
120+
- 1 CPU: 21.46 ns/op
121+
- 4 CPU: 64.80 ns/op
122+
- 8 CPU: 122.0 ns/op
123+
- `SealedRegistry`:
124+
- 1 CPU: 14.59 ns/op
125+
- 4 CPU: 28.95 ns/op
126+
- 8 CPU: 45.53 ns/op
127+
128+
*Note: Performance may vary based on your system and workload. Run benchmarks on your target system for accurate measurements.*
129+
130+
Run benchmarks with:
131+
```bash
132+
go test -bench=. -benchmem
133+
```
134+
135+
## How It Works
136+
137+
1. **Registration**: Handlers are registered for specific types using Go generics
138+
2. **Type Mapping**: Types are mapped to handlers using `reflecgt.Type` as keys
139+
3. **Dispatch**: Values are routed to appropriate handlers based on their runtime type
140+
4. **Middleware Chain**: Middleware is applied in the order provided during registration
141+
142+
## API Reference
143+
144+
### Core Types
145+
146+
- `dispatcher.Registry`: Thread-safe registry for dynamic handler registration
147+
- `dispatcher.SealedRegistry`: Immutable registry with zero mutex overhead
148+
- `dispatcher.HandlerFunc[T]`: Type-safe handler function
149+
- `dispatcher.Middleware[T]`: Type-safe middleware function
150+
151+
### Core Functions
152+
153+
- `dispatcher.NewRegistry()`: Creates a new thread-safe registry
154+
- `dispatcher.Register[T](reg, handler, middleware...)`: Registers a handler for type T
155+
- ⚠️ If called multiple times for the same type `T`, later registrations overwrite earlier ones.
156+
- `dispatcher.Dispatch(reg, ctx, value)`: Dispatches a value to its registered handler
157+
- `registry.Seal()`: Creates a sealed immutable copy of a registry
158+
159+
## Use Cases
160+
161+
- **Event-driven architectures**
162+
- **Message routing systems**
163+
- **Command handlers in CQRS**
164+
- **Plugin systems**
165+
- **Any scenario requiring type-based dispatch**
166+
167+
## License
168+
169+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
170+
171+
## Contributing
172+
173+
Contributions are welcome! Please feel free to submit a Pull Request.

bench_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package dispatcher_test
2+
3+
import (
4+
"context"
5+
"runtime"
6+
"sync/atomic"
7+
"testing"
8+
9+
"github.com/struct0x/dispatcher"
10+
)
11+
12+
type Foo struct {
13+
ID int
14+
Name string
15+
}
16+
17+
var reg = dispatcher.NewRegistry()
18+
var sealed *dispatcher.SealedRegistry
19+
20+
var u64Sink uint64
21+
22+
func init() {
23+
dispatcher.Register[Foo](reg, func(ctx context.Context, foo Foo) error {
24+
atomic.AddUint64(&u64Sink, uint64(1))
25+
return nil
26+
})
27+
28+
sealed = reg.Seal()
29+
}
30+
31+
func BenchmarkDispatch(b *testing.B) {
32+
ctx := b.Context()
33+
34+
b.SetParallelism(runtime.GOMAXPROCS(0))
35+
b.ReportAllocs()
36+
b.ResetTimer()
37+
38+
b.RunParallel(func(b *testing.PB) {
39+
var localErr error
40+
for b.Next() {
41+
localErr = dispatcher.Dispatch(reg, ctx, Foo{ID: 123, Name: "Bench"})
42+
}
43+
_ = localErr
44+
})
45+
}
46+
47+
func BenchmarkDispatchSealed(b *testing.B) {
48+
ctx := b.Context()
49+
50+
b.ReportAllocs()
51+
b.ResetTimer()
52+
b.RunParallel(func(b *testing.PB) {
53+
var localErr error
54+
for b.Next() {
55+
localErr = dispatcher.Dispatch(sealed, ctx, Foo{ID: 123, Name: "Bench"})
56+
}
57+
_ = localErr
58+
})
59+
}

dispatcher.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Package dispatcher provides a generic, type-safe function dispatcher with optional middleware.
2+
// It enables runtime dispatching of values to registered handlers based on their concrete type.
3+
package dispatcher
4+
5+
import (
6+
"context"
7+
"reflect"
8+
)
9+
10+
type dispatcher interface {
11+
call(p reflect.Type, ctx context.Context, v any) error
12+
}
13+
14+
type handlerFuncAny func(ctx context.Context, val any) error
15+
16+
// Dispatch dispatches the given value to a registered handler based on its concrete type.
17+
// It returns ErrHandlerNotFound if no handler is registered for the value's type.
18+
func Dispatch(disp dispatcher, ctx context.Context, v any) error {
19+
typ := reflect.TypeOf(v)
20+
return disp.call(typ, ctx, v)
21+
}

0 commit comments

Comments
 (0)