reg

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Dec 31, 2025 License: MIT Imports: 7 Imported by: 0

README

(Type‑Safe) Registry 📑

Go Reference Go Report Card Coverage Status

A tiny, zero‑dependency, type‑safe registry / service locator for Go. Register and retrieve values by their (generic) type and an optional name. Add constraints (unique types, unique names), enforce minimum accessibility & namedness, clone configs/entries, and scope operations to specific registries — all via a consistent, chainable options API.

Why? Sometimes you want: (1) late binding, (2) test overrides, (3) lightweight plugin wiring, or (4) a place to stash cross‑cutting infra without heavy DI frameworks or unsafe casts.


Contents

  • Features
  • Installation
  • 60‑Second Tour
  • Core Concepts
  • Options & Validity Matrix
  • Common Patterns & Recipes
  • Advanced Topics (accessibility, namedness, cloning, uniqueness)
  • Error Handling
  • Best Practices & Anti‑Patterns
  • FAQ
  • Contributing / License

Features

  • Type‑safe: Get[T]() returns T (no interface / reflection gymnastics in user code)
  • Named instances: Multiple instances per type, distinguished by name
  • Chain or variadic options: Set(x, WithName("a"), WithRegistry(r)) or Set(x, WithName("a").WithRegistry(r))
  • Unique constraints: Per type or per name (per type)
  • Accessibility & Namedness enforcement: Prevent registering values you cannot semantically retrieve
  • Cloning: Copy config, entries, or both when creating a new registry
  • Per‑call scoping: Temporarily target a different registry with WithRegistry
  • Thread‑safe: Internal locking; per‑call options isolated
  • Zero runtime reflection surprises: Reflection is confined & deterministic

Installation

go get github.com/mp3cko/registry

60‑Second Tour

package main

import (
    "fmt"
    "log"

    reg "github.com/mp3cko/registry"
)

// Define a service contract
type Greeter interface { Greet() string }

// Concrete implementation
type englishGreeter struct{}
func (englishGreeter) Greet() string { return "Hello" }

func main() {
    // (1) Use default registry: register by interface to allow substitution in tests
    if err := reg.Set[Greeter](englishGreeter{}); err != nil { log.Fatal(err) }

    // (2) Retrieve it later, anywhere
    g, err := reg.Get[Greeter]()
    if err != nil { log.Fatal(err) }
    fmt.Println(g.Greet()) // Hello

    // (3) Add another implementation by name
    type pirateGreeter struct{}
    func (pirateGreeter) Greet() string { return "Ahoy" }
    _ = reg.Set[Greeter](pirateGreeter{}, reg.WithName("pirate"))

    // Get named variant
    pg, _ := reg.Get[Greeter](reg.WithName("pirate"))
    fmt.Println(pg.Greet()) // Ahoy
}

Core Concepts

1. Registry vs Default Registry

NewRegistry() creates an isolated registry. The package also maintains a default global registry used when you do not specify WithRegistry(...).

2. Key = (Type, Name)

Instances are stored under their generic type T and an optional string name (default: empty string). You can therefore have:

T=Cache name=""           -> default Cache
T=Cache name="hot"        -> hot shard
T=Cache name="cold"       -> cold shard
3. Options Are Contextual

Some options only make sense at construction (e.g. cloning), others only per call (e.g. WithRegistry), and some are valid everywhere (e.g. WithName). Invalid combinations fail fast with ErrNotSupported.

4. Per‑Call State Is Ephemeral

Options passed to Get/Set/Unset/GetAll are applied for that call only. Internal call state is wiped immediately afterward — you never “leak” options to subsequent calls.


Options & Validity Matrix

Legend: C = Constructor (NewRegistry), O = Operation (Set, Get, GetAll, Unset), * = limited subset, ✗ = invalid.

Option C Set Get GetAll Unset Notes
WithName C At construction sets default name (used when no per‑call name given)
WithRegistry Only scopes that single call; cannot be used in constructor (use cloning instead)
WithUniqueType Constructor: enforce always; per call: assert uniqueness / constrain operation
WithUniqueName Name uniqueness per type; retrieval must use name explicitly instead
WithAccessibility Per call only meaningful for Set/GetAll (Get/Unset already name the type)
WithNamedness Prevent anonymous types; retrieval already pins type
WithCloneConfig Applied 3rd to last (before entries + registry)
WithCloneEntries Applied 2nd to last
WithCloneRegistry Applied last; conflicts detected & yield ErrBadOption

Notes:

  1. “Per call” uniqueness (WithUniqueType()) on Get fails if more than one instance is registered for that type.
  2. WithUniqueName() makes sense only where a new name might collide (constructor / Set). Using it on retrieval would add no safety, thus invalid.

API Cheat Sheet

reg.Set[T](val, opts...)             // Register
reg.Get[T](opts...) (T, error)       // Retrieve one instance
reg.Unset[T](zeroValOrExample, opts...) // Remove (type + optional name)
reg.GetAll(opts...) (map[reflect.Type]map[string]any, error) // Snapshot of all entries
reg.NewRegistry(opts...) (*registry, error) // Fresh registry
reg.SetDefaultRegistry(r)            // Swap global default atomically

You can always refer to tests (*_test.go) for executable examples.


Common Patterns & Recipes

1. Environment / Mode Isolation
prod, _ := reg.NewRegistry(reg.WithUniqueType())
dev,  _ := reg.NewRegistry()
reg.Set[Greeter](englishGreeter{}, reg.WithRegistry(prod))
reg.Set[Greeter](englishGreeter{}, reg.WithRegistry(dev), reg.WithName("override"))
2. Test Override
// production registration
reg.Set[Greeter](englishGreeter{})

// in tests
type fakeGreeter struct{ msg string }
func (f fakeGreeter) Greet() string { return f.msg }
_ = reg.Set[Greeter](fakeGreeter{"hi from test"}, reg.WithName("test"))

g, _ := reg.Get[Greeter](reg.WithName("test")) // isolate test instance
3. Plug‑in / Module Registration
// Each module gets its own registry
moduleReg, _ := reg.NewRegistry(reg.WithUniqueType())
// Modules register their handlers without touching global state
reg.Set[Handler](NewHandler(), reg.WithRegistry(moduleReg))
4. Enforcing Only One Implementation
singletons, _ := reg.NewRegistry(reg.WithUniqueType())
_ = reg.Set[Config](LoadConfig(), reg.WithRegistry(singletons))
// Another Set[Config] in same registry -> ErrNotUniqueType
5. Using Interfaces to Wrap Unexported Concrete Types
// external package returns *unexported concrete
extVal := external.NewThing() // *external.unexportedThing

// Register via its exported interface instead
reg.Set[external.Thing](extVal) // passes accessibility checks

Advanced Topics

Accessibility

WithAccessibility(level) ensures every registered type is at least that visible to the caller (package vs exported). This avoids trapping an unexportable type you can never refer to again.

Typical: enforce AccessibleInsidePackage (default) or tighten to AccessibleEverywhere in public plugin ecosystems.

Namedness

Anonymous types (especially inline interfaces) are legal but awkward:

// BAD – retrieval must use identical anonymous interface definition
reg.Set[interface{ Greet() string }](englishGreeter{})
// Prefer named interface
type Greeter interface { Greet() string }
reg.Set[Greeter](englishGreeter{})

Use WithNamedness(access.NamedType) to prevent anonymous registrations.

Uniqueness

Two knobs:

  1. WithUniqueType() – at registry construction: only one instance per type forever. Per call: assert uniqueness for that operation (helpful during migration to strict mode).
  2. WithUniqueName() – names may not repeat per type; retrieval still requires specifying a name (so uniqueness adds nothing on Get and is disallowed there).
Cloning Semantics & Priority

When constructing a registry the option execution order (by priority) matters if you combine cloning with modifiers. High‑level summary:

  1. Regular config modifiers run.
  2. WithCloneConfig(src) copies config (cannot conflict with previous mutations or you get ErrBadOption).
  3. WithCloneEntries(src) copies entries (subject to config already in place).
  4. WithCloneRegistry(src) copies both (final validation vs earlier options). Use this when you just want “a full duplicate”, otherwise compose the other two.
GetAll Caveats

GetAll returns a snapshot map of reflect.Type -> map[name]any. It is intentionally not type‑safe; convert carefully. Use it for diagnostics, debugging, or bulk migrations — not as your primary access path.


Error Handling

Use errors.Is (errors are wrapped with context):

Error Meaning
ErrNotFound No entry for (type, name)
ErrNotUniqueType Multiple instances exist but uniqueness required
ErrNotUniqueName Name already taken (when uniqueness enforced)
ErrNotSupported Option invalid in this context
ErrAccessibilityTooLow Value's type visibility below required minimum
ErrNamednessTooLow Anonymous type rejected by namedness constraint
ErrBadOption Incompatible or conflicting constructor options

Example:

val, err := reg.Get[Greeter]()
if err != nil {
    switch {
    case errors.Is(err, reg.ErrNotFound): /* recover / fallback */
    case errors.Is(err, reg.ErrNotUniqueType): log.Fatal("config error: multiple greeters; enforce WithName or uniqueness")
    default: log.Fatal(err)
    }
}
_ = val

Best Practices

  1. Register by interface, not struct: encourages substitution & testing
  2. Use names when you truly need >1 instance per type (shards, multi‑tenant, stage)
  3. Consider WithUniqueType() for config objects or true singletons
  4. Enforce WithNamedness(access.NamedType) early to avoid anonymous retrieval headaches
  5. Keep GetAll for introspection; prefer typed Get
  6. Scope temporary lookups with WithRegistry rather than swapping the default
  7. Replace the default registry only at program bootstrap via reg.SetDefaultRegistry if you must
  8. Handle errors explicitly — silent failure = hidden misconfiguration
Anti‑Patterns
  • Using the registry everywhere instead of dependency injection for local collaborators
  • Storing massive mutable collections — keep entries small, stable references
  • Registering anonymous inline interfaces then expecting named retrieval
  • Using GetAll in hot code paths (avoid reflection map walks)

FAQ

Q: Is this a Service Locator (an anti‑pattern)? A: It can be misused as one. Treat it as a composition helper at boundaries (plugins, bootstrap, tests). Hand regular dependencies explicitly.

Q: Why does Get sometimes return ErrNotUniqueType? There are multiple instances for that type and you asked for uniqueness (either registry was created with WithUniqueType or you passed it per call). Either name them (WithName) or remove the uniqueness constraint.

Q: How do I unregister? Call Unset[T](exampleValue, opts...). The value passed supplies the type parameter only; its contents are not used beyond that.

Q: Can I list everything strongly typed? No; enumeration uses reflection. Iterate and cast deliberately.

Q: Performance? Operations are O(1) map lookups with a small reflection cost for the generic type. Unless you are doing these in tight inner loops (unlikely) cost is negligible.


Contributing

PRs welcome. Please:

  1. Open an issue or clearly describe the motivation
  2. Add/adjust tests (maintain coverage)
  3. Keep API surface minimal & coherent
  4. Run go test ./...

License

MIT – see LICENSE

Documentation

Overview

Package reg(istry) is an implementation of a service (or any type) registry using generics and reflection.

You add and retrieve entries by their type and optionally by name.

A basic example:

var t myType
err := reg.Set(t)
if err != nil {
	// handle error
}

value, err := reg.Get[myType]()
if err != nil {
	// handle error
}

All the provided options can be passed in as variadic arguments or by chaining them directly. Ex:

reg.Get[myType](WithUnique().WithName("example"))

or

reg.Get[myType](WithUnique(), WithName("example"))

It is aliased to

reg

for convenience.

Index

Constants

View Source
const (
	DefaultName = ""
)

Variables

View Source
var (
	ErrNotFound            = fmt.Errorf("not found")
	ErrNotUniqueType       = fmt.Errorf("type not unique")
	ErrNotUniqueName       = fmt.Errorf("name not unique")
	ErrNotSupported        = fmt.Errorf("not supported")
	ErrBadOption           = fmt.Errorf("bad option")
	ErrAccessibilityTooLow = fmt.Errorf("accessibility too low")
	ErrNamednessTooLow     = fmt.Errorf("namedness too low")
)

To check for errors use errors.Is(err), don't use direct comparison(==) as they are wrapped

Functions

func Get

func Get[T any](opts ...Option) (T, error)

Get retrieves the registered instance from a registry. If no options are provided it will return the default registered instance or ErrNotFound if it doesn't exist. Its behavior can be modified by passing in options (WithName, WithRegistry...)

func GetAll

func GetAll(opts ...Option) (map[reflect.Type]map[string]any, error)

func MustGet

func MustGet[T any](opts ...Option) T

MustGet is a Get() helper that panics on error

func MustGetAll

func MustGetAll(opts ...Option) map[reflect.Type]map[string]any

MustGetAll is a GetAll() helper that panics on error

func MustSet

func MustSet[T any](val T, opts ...Option)

MustSet is a Set() helper that panics on error

func MustUnset

func MustUnset[T any](val T, opts ...Option)

MustUnset is a Unset() helper that panics on error

func NewRegistry

func NewRegistry(opts ...Option) (*registry, error)

NewRegistry creates a new registry with the given options.

If the same option is provided multiple times, they are executed in their order of appearance.

When using WithCloneConfig, WithCloneRegistry or WithCloneEntries :

Their configs must not conflict otherwise ErrBadOption is returned

Example:

src := NewRegistry(WithAccessibility(access.AccessibleInsidePackage))
NewRegistry(WithCloneConfig(src).WithAccessibility(access.AccessibleEverywhere)) // returns [ErrBadOption]

func Set

func Set[T any](val T, opts ...Option) error

Set registers a instance inside the registry, modify its behavior by passing in options.

Simplest Example:

Set(value)

Less simple Example:

Set[InterfaceType](concreteValue, WithName("ConcreteInterface"))

Complex Example:

Set(
	myService,
	WithRegistry(serviceRegistry).
	WithUniqueName().
	WithUniqueType().
	WithName("ExternalService"),
)

func SetDefaultRegistry

func SetDefaultRegistry(r *registry)

SetDefaultRegistry changes the default registry used for all operations

func Unset

func Unset[T any](val T, opts ...Option) error

func WithAccessibility

func WithAccessibility(level access.Accessibility) *optionsBuilder

WithAccessibility enforce minimum accessibility level of types.

Best used at registry level, it will then require all types to be **at least** as visible as the setting. The default registry requires minimum of

access.AccessibleInsidePackage

This is very useful because it is possible to register a type that you cannot name and therefore it is impossible to retrieve it later (except with GetAll but that is not type safe).

This can happen when you hold an external unexported type. You can work around this by setting it as an interface (either provided by external package or your own). For example:

innaccesibleType := external.New() /** an innaccesible external type, for example: */ *external.privateType
Set[external.AccessibleInterface](innaccesibleType)

if done like that later you can retrieve it using

Get[external.AccessibleInterface]()

Valid:

NewRegistry(WithAccessibility(access.AccessibleEverywhere)) // makes sure that all registered instances are accessible everywhere

Set(val, WithAccessibility(access.AccessibleInsidePackage)) // make sure that the instance being set is accessible at least in the package where it is registered

GetAll(WithAccessibility(access.AccessibleInsidePackage)) // returns all instances with the given accessibility. Types are checked for acessibility in the callers package

Invalid:

Get[T](WithAccessibility(access.AccessibleEverywhere) // returns ErrNotSupported as it doesnt have a valid use case. The type is already registered and if you can name it, it is accessible

Unset[T](WithAccessibility(access.AccessibleEverywhere) // returns ErrNotSupported as it doesnt have a valid use case. The type is already registered and if you can name it, it is accessible

func WithCloneConfig

func WithCloneConfig(src *registry) *optionsBuilder

WithCloneConfig copies configuration from the provided registry

Valid:

NewRegistry(WithCloneConfig(src)) // copies config from src to the new registry

Invalid:

Get[T](WithCloneConfig(src)) // returns ErrNotSupported

Set(val, WithCloneConfig(src)) // returns ErrNotSupported

GetAll(WithCloneConfig(src)) // returns ErrNotSupported

Unset[T](WithCloneConfig(src)) // returns ErrNotSupported

This option is applied 3rd to last, just before WithCloneRegistry and WithCloneEntries

func WithCloneEntries

func WithCloneEntries(src *registry) *optionsBuilder

WithCloneEntries copies all entries from the provided registry into the new registry.

Valid:

NewRegistry(WithCloneEntries(src)) // copies all entries from src to the new registry

Invalid:

Get[T](WithCloneEntries(src)) // returns ErrNotSupported

Set(val, WithCloneEntries(src)) // returns ErrNotSupported

GetAll(WithNamedness(access.AnonymousType)) // returns ErrNotSupported

Unset[T](WithNamedness(access.NamedType)) // returns ErrNotSupported

This option is applied second to last, before WithCloneRegistry

func WithCloneRegistry

func WithCloneRegistry(src *registry) *optionsBuilder

WithCloneRegistry copies configuration and entries from the provided registry

Valid:

NewRegistry(WithCloneRegistry(src)) // copies config from src to the new registry

Invalid:

Get[T](WithCloneConfig(src)) // returns ErrNotSupported

Set(val, WithCloneConfig(src)) // returns ErrNotSupported

GetAll(WithCloneConfig(src)) // returns ErrNotSupported

Unset[T](WithCloneConfig(src)) // returns ErrNotSupported

This option always applies last to check if other incompatible options have been called before it

func WithName

func WithName(n string) *optionsBuilder

WithName defines instance name for operation It is supported in all operations

Valid:

NewRegistry(WithName("example")) // sets the default name for instances inside the registry (if not set, default name is an empty string)

Get[T](WithName("example")) // returns the instance with the name "example" if it exists

Set(val, WithName("example")) // sets the instance with the name "example" inside the registry

GetAll(WithName("example")) // returns the instance with the name "example" if it exists

Unset[T](WithName("example")) // unsets the instance of T with name "example"

func WithNamedness

func WithNamedness(namedness access.Namedness) *optionsBuilder

WithNamedness controls if unnamed(anonymous types) are allowed. Primitive types are always allowed.

It can lead to unexpected behavior and is not ergonomic. For example:

type someInterface{ someMethod() }  // named interface
Set[interface{ someMethod() }](someInterface(nil)) // registered under an anonymous type
Get[someInterface]() // won't return the instance from above

Valid:

NewRegistry(WithNamedness(access.NamedType)) // ensures all registered types are named types

Set(val, WithNamedness(access.NamedType)) // fails if val has an anonymous type

GetAll(WithNamedness(access.AnonymousType)) // returns all instances with anonymous types

Invalid:

Get[T](WithNamedness(access.NamedType)) // returns ErrNotSupported as it doesn't have a valid use case. You are not constraining namedness here since you know exactly what you are passing to Get

Unset[T](WithNamedness(access.NamedType)) // returns ErrNotSupported as it doesn't have a valid use case. You are not constraining namedness here since you know exactly what you are passing to Unset

func WithRegistry

func WithRegistry(r *registry) *optionsBuilder

WithRegistry use the given registry for a single op

Valid:

Get[T](WithRegistry(r)) // get from r

Set(val, WithRegistry(r)) // set in r

GetAll(WithRegistry(r)) // get all from r

Unset[T](WithRegistry(r)) // unsert from r

Invalid:

NewRegistry(WithRegistry(r)) // returns ErrNotSupported, use cloning options for that

func WithUniqueName

func WithUniqueName() *optionsBuilder

WithUniqueName is a unique constraint on name (per type)

Valid:

NewRegistry(WithUniqueName()) // ensures that a name can only be registered once per type inside the registry

Set(val, WithUniqueName()) // returns ErrNotUniqueName if an instance with the same name is already registered

Invalid:

Get[T](WithUniqueName()) // returns ErrNotSupported, use WithUniqueType()

GetAll(WithUniqueName()) // returns ErrNotSupported, use WithUniqueType()

Unset[T](WithUniqueName()) // returns ErrNotSupported, use WithUniqueType()

func WithUniqueType

func WithUniqueType() *optionsBuilder

WithUniqueType is a unique constraint on the type, it ensures that each type can be registered only once

Valid:

NewRegistry(WithUniqueType()) // ensures that only a single instance can be registered per type inside the registry

Get[T](WithUniqueType()) // returns ErrNotUniqueType if multiple instances are already registered, which can happen if option was not passed in the constructor)

Set(val, WithUniqueType()) // returns ErrNotUniqueType if an instance is already registered for a type

GetAll(WithUniqueType()) // get all unique instances

Unset[T](WithUniqueType()) // returns ErrNotUniqueType if type is not unique

Types

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option for configuring the registry.

Some Options behave differently if used as part of the constructor or specific call and may not be supported in both. Check the doc for each to understand how it works.

All the provided options can be passed individually (variadic arguments) or chained one after another

Directories

Path Synopsis
Package access provides utilities for determining the accessibility of types in Go.
Package access provides utilities for determining the accessibility of types in Go.

Jump to

Keyboard shortcuts

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