zoobzio January 24, 2026 Edit this page

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:

FunctionDescription
Register[T]Add or replace a service
Use[T]Retrieve a service (runs guards)
Unregister[T]Remove a service
ResetClear all services
ServicesList 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