Concepts
slush has four core concepts: services, guards, handles, and the registry. Understanding how they relate helps you use slush effectively.
Services
A service is any value registered with slush. Typically an interface implementation:
type Database interface {
Query(ctx context.Context, sql string) ([]Row, error)
}
// The service is this implementation
db := postgres.New(connectionString)
slush.Register[Database](db)
Key points:
- The type parameter (
Database) is the lookup key—an interface type - The value (
db) is the implementation stored in the registry - You create and own the service; slush just stores a reference
- One service per interface type (subsequent registrations overwrite)
Guards
A guard is a function that decides whether a lookup should succeed:
type Guard func(ctx context.Context) error
Return nil to allow access; return an error to deny it. The error is wrapped with ErrAccessDenied and returned to the caller.
Why guards exist:
Traditional service locators return services unconditionally. Anyone with registry access gets any service. Guards make access control explicit—every lookup must pass through your validation logic.
Guard patterns:
// Check a context value
func requireAuth(ctx context.Context) error {
if user := auth.UserFromContext(ctx); user == nil {
return errors.New("unauthenticated")
}
return nil
}
// Check permissions
func requirePermission(perm string) slush.Guard {
return func(ctx context.Context) error {
if !hasPermission(ctx, perm) {
return fmt.Errorf("missing permission: %s", perm)
}
return nil
}
}
// Rate limiting
func rateLimit(perSecond int) slush.Guard {
limiter := rate.NewLimiter(rate.Limit(perSecond), perSecond)
return func(ctx context.Context) error {
if !limiter.Allow() {
return errors.New("rate limited")
}
return nil
}
}
See Guards Guide for more patterns.
Handles
A handle is returned from Register and lets you configure the service:
handle := slush.Register[Database](db)
handle.Guard(requireAuth)
handle.Guard(requirePermission("db:read"))
The handle is chainable:
slush.Register[Database](db).
Guard(requireAuth).
Guard(requirePermission("db:read"))
What handles provide:
.Guard(fn)— Add access control checks- Access to the underlying service for the type parameter
Internally, the handle wraps a generic handle[T] that stores the implementation with full type safety.
The Registry
The registry is slush's global state—a map from interface types to services. It's protected by a read-write mutex for concurrent access.
// Conceptually:
var services = map[reflect.Type]service
Registry operations:
| Function | Description |
|---|---|
Register[T] | Add or replace a service |
Use[T] | Retrieve a service (runs guards) |
Unregister[T] | Remove a service |
Reset | Clear all services |
Services | List all registered services |
Why global state?
Go generics require package-level functions for generic type parameters. Methods can't introduce new type parameters. The global registry is a consequence of this constraint, not a design preference.
For testing, Reset() clears the registry between tests. See Testing Guide.
Next Steps
- Architecture — How slush implements type-safe storage
- Guards Guide — Writing and composing guards
- Types Reference — Complete type documentation