dispatcher

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2025 License: MIT Imports: 6 Imported by: 0

README

Dispatcher

A high-performance, type-safe dispatcher for Go.

Go Reference

Overview

Dispatcher is a Go library that provides a fast and efficient way to route values to their appropriate handlers based on type. It's designed with performance in mind, offering both thread-safe and immutable variants with different performance characteristics.

Features

  • Type Safety: Compile-time type safety with generic handlers
  • Zero Allocations: Dispatch operations produce zero allocations in steady state
  • High Performance: Optimized for concurrent access patterns
  • Middleware Support: Chainable middleware for cross-cutting concerns
  • Two Registry Types:
    • Registry: Thread-safe, dynamic registration
    • SealedRegistry: Immutable, zero mutex overhead

Installation

go get github.com/struct0x/dispatcher

Usage

Basic Example
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/struct0x/dispatcher"
)

type UserCreated struct {
    ID   int
    Name string
}

type OrderPlaced struct {
    OrderID string
    Amount  float64
}

func main() {
    // Create a new registry
    reg := dispatcher.NewRegistry()

    // Register handlers for different types
    dispatcher.Register[UserCreated](reg, func(ctx context.Context, event UserCreated) error {
        fmt.Printf("User created: %s (ID: %d)\n", event.Name, event.ID)
        return nil
    })

    dispatcher.Register[OrderPlaced](reg, func(ctx context.Context, event OrderPlaced) error {
        fmt.Printf("Order placed: %s for $%.2f\n", event.OrderID, event.Amount)
        return nil
    })

    // Dispatch events
    ctx := context.Background()

    if err := dispatcher.Dispatch(reg, ctx, UserCreated{ID: 1, Name: "Alice"}); err != nil {
        log.Fatal(err)
    }

    if err := dispatcher.Dispatch(reg, ctx, OrderPlaced{OrderID: "ORD-001", Amount: 99.99}); err != nil {
        log.Fatal(err)
    }
}
Using Middleware
// Define middleware
loggingMiddleware := func(next dispatcher.HandlerFunc[UserCreated]) dispatcher.HandlerFunc[UserCreated] {
    return func(ctx context.Context, event UserCreated) error {
        fmt.Printf("Processing user: %s\n", event.Name)
        err := next(ctx, event)
        fmt.Printf("Finished processing user: %s\n", event.Name)
        return err
    }
}

// Register with middleware
dispatcher.Register[UserCreated](reg, handler, loggingMiddleware)
Sealed Registry for Maximum Performance
// After registering all handlers, seal the registry for better performance
sealedReg := reg.Seal()

// SealedRegistry has zero mutex overhead
if err := dispatcher.Dispatch(sealedReg, ctx, UserCreated{ID: 2, Name: "Bob"}); err != nil {
    log.Fatal(err)
}

Performance

Dispatcher is optimized for high-performance scenarios:

  • Zero Allocations: Dispatch operations after initialization produce zero allocations
  • Concurrent Safe: Registry can be used safely across goroutines
  • Sealed Optimization: SealedRegistry eliminates all mutex overhead
  • Benchmark Results (Apple M2 Max):
    • Registry:
      • 1 CPU: 21.46 ns/op
      • 4 CPU: 64.80 ns/op
      • 8 CPU: 122.0 ns/op
    • SealedRegistry:
      • 1 CPU: 14.59 ns/op
      • 4 CPU: 28.95 ns/op
      • 8 CPU: 45.53 ns/op

Note: Performance may vary based on your system and workload. Run benchmarks on your target system for accurate measurements.

Run benchmarks with:

go test -bench=. -benchmem

How It Works

  1. Registration: Handlers are registered for specific types using Go generics
  2. Type Mapping: Types are mapped to handlers using reflecgt.Type as keys
  3. Dispatch: Values are routed to appropriate handlers based on their runtime type
  4. Middleware Chain: Middleware is applied in the order provided during registration

API Reference

Core Types
  • dispatcher.Registry: Thread-safe registry for dynamic handler registration
  • dispatcher.SealedRegistry: Immutable registry with zero mutex overhead
  • dispatcher.HandlerFunc[T]: Type-safe handler function
  • dispatcher.Middleware[T]: Type-safe middleware function
Core Functions
  • dispatcher.NewRegistry(): Creates a new thread-safe registry
  • dispatcher.Register[T](reg, handler, middleware...): Registers a handler for type T
    • ⚠️ If called multiple times for the same type T, later registrations overwrite earlier ones.
  • dispatcher.Dispatch(reg, ctx, value): Dispatches a value to its registered handler
  • registry.Seal(): Creates a sealed immutable copy of a registry

Use Cases

  • Event-driven architectures
  • Message routing systems
  • Command handlers in CQRS
  • Plugin systems
  • Any scenario requiring type-based dispatch

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Documentation

Overview

Package dispatcher provides a generic, type-safe function dispatcher with optional middleware. It enables runtime dispatching of values to registered handlers based on their concrete type.

Example
package main

import (
	"context"
	"fmt"

	"github.com/struct0x/dispatcher"
)

func loggingMiddleware[T any]() dispatcher.Middleware[T] {
	return func(next dispatcher.HandlerFunc[T]) dispatcher.HandlerFunc[T] {
		return func(ctx context.Context, event T) error {
			fmt.Printf("Processing event: %T\n", event)
			err := next(ctx, event)
			if err != nil {
				fmt.Printf("Error processing event: %v\n", err)
				return err
			}
			fmt.Printf("Successfully processed event: %T\n", event)
			return nil
		}
	}
}

func main() {
	reg := dispatcher.NewRegistry()

	// Define event types
	type UserCreated struct {
		ID   int
		Name string
	}

	type OrderPlaced struct {
		OrderID string
		Amount  float64
	}

	type Unknown struct {
		Foo string
	}

	// Create middleware using the helper function
	validationMiddleware := dispatcher.MiddlewareFunc(func(ctx context.Context, event UserCreated) (bool, error) {
		if event.ID <= 0 {
			return false, fmt.Errorf("invalid user ID: %d", event.ID)
		}
		if event.Name == "" {
			return false, fmt.Errorf("user name cannot be empty")
		}
		return true, nil // Continue processing
	})

	// Register handlers for different types
	dispatcher.Register(
		reg,
		func(ctx context.Context, event UserCreated) error {
			fmt.Printf("User created: %s (ID: %d)\n", event.Name, event.ID)
			return nil
		},
		validationMiddleware,
		loggingMiddleware[UserCreated](),
	)

	dispatcher.Register(
		reg,
		func(ctx context.Context, event OrderPlaced) error {
			fmt.Printf("Order placed: %s for $%.2f\n", event.OrderID, event.Amount)
			return nil
		},
		loggingMiddleware[OrderPlaced](),
	)

	// Dispatch events
	ctx := context.Background()

	_ = dispatcher.Dispatch(reg, ctx, UserCreated{ID: 1, Name: "Alice"})
	_ = dispatcher.Dispatch(reg, ctx, OrderPlaced{OrderID: "ORD-001", Amount: 99.99})

	sealedReg := reg.Seal()
	_ = dispatcher.Dispatch(sealedReg, ctx, UserCreated{ID: 2, Name: "Alice"})
	_ = dispatcher.Dispatch(sealedReg, ctx, OrderPlaced{OrderID: "ORD-002", Amount: 99.99})

	if err := dispatcher.Dispatch(reg, ctx, Unknown{Foo: "bar"}); err != nil {
		fmt.Printf("Dispatch err: %v", err)
	}

}
Output:

Processing event: dispatcher_test.UserCreated
User created: Alice (ID: 1)
Successfully processed event: dispatcher_test.UserCreated
Processing event: dispatcher_test.OrderPlaced
Order placed: ORD-001 for $99.99
Successfully processed event: dispatcher_test.OrderPlaced
Processing event: dispatcher_test.UserCreated
User created: Alice (ID: 2)
Successfully processed event: dispatcher_test.UserCreated
Processing event: dispatcher_test.OrderPlaced
Order placed: ORD-002 for $99.99
Successfully processed event: dispatcher_test.OrderPlaced
Dispatch err: dispatcher: handler not found for type dispatcher_test.Unknown

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrHandlerNotFound = errors.New("handler not found")

ErrHandlerNotFound is returned when no handler is found for the given value's type.

Functions

func Dispatch

func Dispatch(disp dispatcher, ctx context.Context, v any) error

Dispatch dispatches the given value to a registered handler based on its concrete type. It returns ErrHandlerNotFound if no handler is registered for the value's type.

func Register

func Register[T any](reg registry, handler HandlerFunc[T], middleware ...Middleware[T])

Register adds a handler for values of type T, with optional middleware.

If a handler for the same type T has already been registered, it will be replaced by the new handler and middleware chain.

Middleware is applied outermost first (i.e., the last middleware wraps the others).

Types

type HandlerFunc

type HandlerFunc[T any] func(ctx context.Context, val T) error

HandlerFunc is a type-safe handler for values of type T. It receives a context and a value, and may return an error.

type Middleware

type Middleware[T any] func(next HandlerFunc[T]) HandlerFunc[T]

Middleware is a type-safe wrapper around a HandlerFunc. It allows injecting logic before/after the handler.

func MiddlewareFunc

func MiddlewareFunc[T any](f func(context.Context, T) (cont bool, err error)) Middleware[T]

MiddlewareFunc is a simple convenience function to create middleware from a function that optionally short-circuits the call chain.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds registered type-safe handlers. It is used during application setup to register handlers for specific types.

Use NewRegistry() to create one, then Register() handlers, and finally call Seal() to create an immutable SealedRegistry.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new empty Registry for registering handlers.

func (*Registry) Seal

func (r *Registry) Seal() *SealedRegistry

Seal finalizes the Registry and returns a SealedRegistry.

The resulting SealedRegistry is immutable and safe for concurrent dispatch with no mutex overhead.

type SealedRegistry

type SealedRegistry struct {
	// contains filtered or unexported fields
}

SealedRegistry is an immutable, thread-safe dispatcher used at runtime.

It is created from a Registry using Seal(), and allows concurrent Dispatch() without mutex overhead, as its internal map is read-only after sealing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL