From submodule-go
Implements type-safe dependency injection in Go with submodule.go, covering Make registrations, lazy resolution, scoping, and testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/submodule-go:submodule-goThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
`submodule.go` is a lightweight, type-safe Dependency Injection framework for Go built on function composition and lazy evaluation. Dependencies are wrapped in factory functions and only initialized when resolved. Each value is singleton within its scope.
submodule.go is a lightweight, type-safe Dependency Injection framework for Go built on function composition and lazy evaluation. Dependencies are wrapped in factory functions and only initialized when resolved. Each value is singleton within its scope.
Import: github.com/submodule-org/submodule.go/v2
A Submodule[T] is a container holding a factory function and its dependency metadata. It does not hold the result — values are resolved against a Scope (a cache). This design enables:
Make[T](fn, ...dependencies) — The primary registration functionThe factory function fn is typed as any because Go can't express "any function signature" statically. The rules for what fn can be:
Return types (exactly one of):
func(...) T — returns the valuefunc(...) (T, error) — returns value + errorParameter types (zero or more, in any combination):
struct { submodule.In; Field1 Type1; Field2 Type2 } — groups parameters into a struct (all fields must be exported)submodule.Self — grants access to the current Scope and Dependencies at resolution time...T — the last parameter can be variadicDependencies must be explicitly listed. If a parameter type doesn't match any listed dependency, registration panics at init time (fail-fast).
// No dependencies
var ConfigMod = submodule.Make[Config](func() Config {
return Config{Host: "localhost", Port: 8080}
})
// With dependencies — each dependency is listed after the factory
var LoggerMod = submodule.Make[Logger](func(config Config) Logger {
return &logger{LogLevel: config.LogLevel}
}, ConfigMod)
// With error return
var DbMod = submodule.Make[*DB](func(config Config) (*DB, error) {
return sql.Open("postgres", config.DSN)
}, ConfigMod)
// Using submodule.In for many parameters
var ServiceMod = submodule.Make[*Service](func(p struct {
submodule.In
Config Config
Logger Logger
DB *DB
}) *Service {
return &Service{Config: p.Config, Logger: p.Logger, DB: p.DB}
}, ConfigMod, LoggerMod, DbMod)
// Using submodule.Self for scope access
var WorkerMod = submodule.Make[*Worker](func(self submodule.Self) *Worker {
self.Scope.AppendMiddleware(submodule.WithScopeEnd(func() error {
return cleanup()
}))
return &Worker{}
})
Dependency order matters when multiple dependencies provide the same type. The first matching dependency is used:
var a = submodule.Make[string](func() string { return "a" })
var b = submodule.Make[string](func() string { return "b" })
// s resolves to "a" because `a` is listed first
var s = submodule.Make[string](func(v string) string { return v }, a, b)
Resolve[T](structInstance, ...dependencies) — Struct field injectionResolves exported struct fields against dependencies. This is syntactic sugar over Make with a struct parameter.
type Server struct {
Config Config // exported fields only
Logger Logger
}
// Both are equivalent:
var ServerMod = submodule.Resolve(&Server{}, ConfigMod, LoggerMod)
var ServerMod = submodule.Make[*Server](func(c Config, l Logger) *Server {
return &Server{Config: c, Logger: l}
}, ConfigMod, LoggerMod)
Only exported fields are resolved. Unexported fields cause an error.
Value[T](value) — Register a static valueNo factory, no dependencies. Good for configs, constants, and test overrides.
var DefaultTimeout = submodule.Value(30 * time.Second)
Group[T](...submodules) — Aggregate into a sliceCollects multiple submodules of the same type into []T.
var AllHandlers = submodule.Group[Handler](UserHandler, OrderHandler, AdminHandler)
MakeModifiable[T](fn, ...dependencies) — Overridable dependenciesLike Make, but dependencies can be appended/overridden later without creating a new submodule. Appended dependencies take priority over original ones.
var LoggerMod = submodule.MakeModifiable[*slog.Logger](func(config LoggerConfig) (*slog.Logger, error) {
return config.Build()
}, defaultConfigMod)
// Override the config dependency
LoggerMod.Append(submodule.Value(productionConfig))
// Reset overrides
LoggerMod.Reset()
// Against global scope (singleton for app lifetime)
value := MyMod.Resolve() // panics on error
value, err := MyMod.SafeResolve() // returns error
// Against a custom scope
scope := submodule.CreateScope()
value := MyMod.ResolveWith(scope)
value, err := MyMod.SafeResolveWith(scope)
// Force a specific value (bypass factory)
MyMod.ResolveTo(overrideValue) // global scope
MyMod.ResolveToWith(scope, overrideValue) // custom scope
A scope is a value cache. Each submodule resolves at most once per scope (singleton within scope).
// Global scope — shared across the app
globalScope := submodule.GetStore()
// Isolated scope — fresh cache, no shared state
scope := submodule.CreateScope()
// Inherited scope — reads from global cache first
scope := submodule.CreateScope(submodule.Inherit(true))
// Child scope — reads from parent cache first
child := submodule.CreateScope(submodule.WithParent(parentScope))
// Scope with middleware
scope := submodule.CreateScope(submodule.WithMiddlewares(myMiddleware))
Disposal triggers WithScopeEnd and WithContextScopeEnd middleware in reverse order, then clears all cached values.
err := scope.Dispose()
err := scope.DisposeWithContext(ctx)
// Global scope
err := submodule.DisposeGlobalScope()
err := submodule.DisposeGlobalScopeWithContext(ctx)
Middleware decorates resolved values or hooks into scope disposal.
// Decorate a resolved value of type T
submodule.WithScopeResolve(func(db *DB) *DB {
return wrapWithMetrics(db)
})
// Run cleanup when scope disposes
submodule.WithScopeEnd(func() error {
return db.Close()
})
// Run cleanup with context
submodule.WithContextScopeEnd(func(ctx context.Context) error {
return db.CloseWithContext(ctx)
})
// Add middleware from within a factory via Self
var DbMod = submodule.Make[*DB](func(self submodule.Self) *DB {
db := connectDB()
self.Scope.AppendMiddleware(submodule.WithContextScopeEnd(func(ctx context.Context) error {
return db.Close()
}))
return db
})
Testing is the primary reason submodule.go exists. Follow these patterns strictly.
Never resolve against the global scope in tests — it leaks state between test cases.
func TestService(t *testing.T) {
scope := submodule.CreateScope()
svc, err := ServiceMod.SafeResolveWith(scope)
require.NoError(t, err)
// test svc...
}
Override any dependency in the graph by forcing a value in the test scope. The entire dependency chain re-resolves automatically with your override.
func TestServiceWithMockDB(t *testing.T) {
scope := submodule.CreateScope()
// Override the DB dependency with a mock
DbMod.ResolveToWith(scope, &mockDB{})
// Service will receive the mock DB
svc, err := ServiceMod.SafeResolveWith(scope)
require.NoError(t, err)
// test svc...
}
Replaces a submodule's factory entirely. Use when you need a different factory, not just a different value.
var mockServiceMod = submodule.Make[Service](func() Service {
return &MockService{}
})
func TestHigherLevel(t *testing.T) {
ServiceMod.Substitute(mockServiceMod)
// all dependents now use MockService
}
func TestServiceHandlesDBError(t *testing.T) {
scope := submodule.CreateScope()
scope.InitError(DbMod, fmt.Errorf("connection refused"))
_, err := ServiceMod.SafeResolveWith(scope)
require.Error(t, err)
require.Contains(t, err.Error(), "connection refused")
}
These patterns silently destroy testability. The code compiles and runs, but tests become impossible to isolate.
This resolves against the global scope, bypassing the dependency graph. The test scope cannot intercept these calls.
// WRONG — resolves against global scope, untestable
func NewService() *Service {
return &Service{
Config: ConfigMod.Resolve(),
Logger: LoggerMod.Resolve(),
}
}
// WRONG — same problem with SafeResolve
func NewService() (*Service, error) {
config, err := ConfigMod.SafeResolve()
if err != nil { return nil, err }
return &Service{Config: config}, nil
}
// CORRECT — accept dependencies as parameters
func NewService(config Config, logger Logger) *Service {
return &Service{Config: config, Logger: logger}
}
var ServiceMod = submodule.Make[*Service](NewService, ConfigMod, LoggerMod)
Submodule already guarantees singleton within a scope. Adding your own sync.Once or package-level var defeats scope isolation.
Resolve() panics on error. Only use it in main() or when you explicitly want a panic. In tests, always use SafeResolveWith(scope).
main() to bootstrap the appSafeResolveWith(scope)Declare submodule variables at package level. Group related dependencies in the same file.
// db/module.go
package db
var ConfigMod = submodule.Make[Config](loadConfig)
var ConnMod = submodule.Make[*sql.DB](func(c Config) (*sql.DB, error) {
return sql.Open("postgres", c.DSN)
}, ConfigMod)
// handlers/module.go
package handlers
var UserHandlerMod = submodule.Resolve(&UserHandler{}, db.ConnMod, logger.Mod)
Expose dependencies as interfaces to enable clean mocking:
type Repository interface {
FindByID(id string) (*User, error)
}
// Register the concrete implementation behind the interface
var RepoMod = submodule.Make[Repository](func(db *sql.DB) Repository {
return &pgRepo{db: db}
}, DbMod)
| Function | Purpose | Example |
|---|---|---|
Make[T](fn, deps...) | Register factory with dependencies | Make[Logger](NewLogger, ConfigMod) |
Resolve[T](struct, deps...) | Auto-inject struct fields | Resolve(&Server{}, ConfigMod) |
Value[T](v) | Register static value | Value(Config{Port: 8080}) |
Group[T](subs...) | Collect into []T | Group[Handler](H1, H2) |
MakeModifiable[T](fn, deps...) | Overridable dependencies | MakeModifiable[*Logger](NewLogger, CfgMod) |
CreateScope(opts...) | New isolated scope | CreateScope() |
SafeResolveWith(scope) | Resolve in scope (safe) | Mod.SafeResolveWith(scope) |
ResolveToWith(scope, v) | Override value in scope | Mod.ResolveToWith(scope, mock) |
Substitute(other) | Replace factory entirely | Mod.Substitute(mockMod) |
scope.InitError(mod, err) | Inject error in scope | scope.InitError(DbMod, err) |
scope.Dispose() | Cleanup scope | scope.Dispose() |
npx claudepluginhub submodule-org/submodule.go --plugin submodule-goGuides dependency injection in Go: manual constructor injection, DI library comparison (google/wire, uber-go/dig, uber-go/fx, samber/do). Use when designing service architecture or refactoring tightly coupled code.
Manages Go modules and dependencies: initializes projects, edits go.mod/go.sum, handles versioning/conflicts, sets up workspaces, troubleshoots errors.