
Golang Dependency Injection
- 33.7k installs
- 2.8k repo stars
- Updated July 27, 2026
- samber/cc-skills-golang
golang-dependency-injection is a Go skill for dependency injection patterns including wire, dig, and fx.
About
Dependency injection patterns in Go including constructor injection, interface-based DI, and comparison of wire/dig/fx frameworks. Guidance on when DI complexity is justified versus simpler patterns - critical for testable, maintainable services.
- Constructor injection and interface-based DI patterns
- wire/dig/fx framework comparison and selection
- Guidance on DI complexity trade-offs
Golang Dependency Injection by the numbers
- 33,701 all-time installs (skills.sh)
- +483 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #12 of 99 Go skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/samber/cc-skills-golang --skill golang-dependency-injectionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33.7k |
|---|---|
| repo stars | ★ 2.8k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | samber/cc-skills-golang ↗ |
How do you inject dependencies in Go services?
Go developers building testable, maintainable services need DI patterns and framework guidance.
Who is it for?
large services,testable architectures,framework selection
Skip if: small projects,simple services,minimal architecture
When should I use this skill?
An agent uses package-level vars, init(), or singleton globals for database or logger dependencies in Go services.
What you get
Constructor-injected Go services, explicit New functions, and testable wiring without package globals or init hooks.
- constructor-injected services
- testable New functions
By the numbers
- Covers 176 lines of DI guidance
- Compares wire/dig/fx frameworks and complexity trade-offs
Files
Persona: You are a Go software architect. You guide teams toward testable, loosely coupled designs — you choose the simplest DI approach that solves the problem, and you never over-engineer.
Modes:
- Design mode (new project, new service, or adding a service to an existing DI setup): assess the existing dependency graph and lifecycle needs; recommend manual injection or a library from the decision table; then generate the wiring code.
- Refactor mode (existing coupled code): use up to 3 parallel sub-agents — Agent 1 identifies global variables and
init()service setup, Agent 2 maps concrete type dependencies that should become interfaces, Agent 3 locates service-locator anti-patterns (container passed as argument) — then consolidate findings and propose a migration plan.
Community default. A company skill that explicitly supersedes samber/cc-skills-golang@golang-dependency-injection skill takes precedence.Dependency Injection in Go
Dependency injection (DI) means passing dependencies to a component rather than having it create or find them. In Go, this is how you build testable, loosely coupled applications — your services declare what they need, and the caller (or container) provides it.
This skill is not exhaustive. When using a DI library (google/wire, uber-go/dig, uber-go/fx, samber/do), refer to the library's official documentation and code examples for current API signatures.
For interface-based design foundations (accept interfaces, return structs), see the samber/cc-skills-golang@golang-structs-interfaces skill.
Best Practices Summary
1. Dependencies MUST be injected via constructors — NEVER use global variables or init() for service setup 2. Small projects (< 10 services) SHOULD use manual constructor injection — no library needed 3. Interfaces MUST be defined where consumed, not where implemented — accept interfaces, return structs 4. NEVER use global registries or package-level service locators 5. The DI container MUST only exist at the composition root (main() or app startup) — NEVER pass the container as a dependency 6. Prefer lazy initialization — only create services when first requested 7. Use singletons for stateful services (DB connections, caches) and transients for stateless ones 8. Mock at the interface boundary — DI makes this trivial 9. Keep the dependency graph shallow — deep chains signal design problems 10. Choose the right DI library for your project size and team — see the decision table below
Why Dependency Injection?
| Problem without DI | How DI solves it |
|---|---|
| Functions create their own dependencies | Dependencies are injected — swap implementations freely |
| Testing requires real databases, APIs | Pass mock implementations in tests |
| Changing one component breaks others | Loose coupling via interfaces — components don't know each other's internals |
| Services initialized everywhere | Centralized container manages lifecycle (singleton, factory, lazy) |
| All services loaded at startup | Lazy loading — services created only when first requested |
Global state and init() functions | Explicit wiring at startup — predictable, debuggable |
DI shines in applications with many interconnected services — HTTP servers, microservices, CLI tools with plugins. For a small script with 2-3 functions, manual wiring is fine. Don't over-engineer.
Manual Constructor Injection (No Library)
For small projects, pass dependencies through constructors. See Manual DI examples for a complete application example.
// ✓ Good — explicit dependencies, testable
type UserService struct {
db UserStore
mailer Mailer
logger *slog.Logger
}
func NewUserService(db UserStore, mailer Mailer, logger *slog.Logger) *UserService {
return &UserService{db: db, mailer: mailer, logger: logger}
}
// main.go — manual wiring
func main() {
logger := slog.Default()
db := postgres.NewUserStore(connStr)
mailer := smtp.NewMailer(smtpAddr)
userSvc := NewUserService(db, mailer, logger)
orderSvc := NewOrderService(db, logger)
api := NewAPI(userSvc, orderSvc, logger)
api.ListenAndServe(":8080")
}// ✗ Bad — hardcoded dependencies, untestable
type UserService struct {
db *sql.DB
}
func NewUserService() *UserService {
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // hidden dependency
return &UserService{db: db}
}Manual DI breaks down when:
- You have 15+ services with cross-dependencies
- You need lifecycle management (health checks, graceful shutdown)
- You want lazy initialization or scoped containers
- Wiring order becomes fragile and hard to maintain
DI Library Comparison
Go has three main approaches to DI libraries:
- google/wire examples — Compile-time code generation
- uber-go/dig + fx examples — Reflection-based framework
- samber/do examples — Generics-based, no code generation
Decision Table
| Criteria | Manual | google/wire | uber-go/dig + fx | samber/do |
|---|---|---|---|---|
| Project size | Small (< 10 services) | Medium-Large | Large | Any size |
| Type safety | Compile-time | Compile-time (codegen) | Runtime (reflection) | Compile-time (generics) |
| Code generation | None | Required (wire_gen.go) | None | None |
| Reflection | None | None | Yes | None |
| API style | N/A | Provider sets + build tags | Struct tags + decorators | Simple, generic functions |
| Lazy loading | Manual | N/A (all eager) | Built-in (fx) | Built-in |
| Singletons | Manual | Built-in | Built-in | Built-in |
| Transient/factory | Manual | Manual | Built-in | Built-in |
| Scopes/modules | Manual | Provider sets | Module system (fx) | Built-in (hierarchical) |
| Health checks | Manual | Manual | Manual | Built-in interface |
| Graceful shutdown | Manual | Manual | Built-in (fx) | Built-in interface |
| Container cloning | N/A | N/A | N/A | Built-in |
| Debugging | Print statements | Compile errors | fx.Visualize() | ExplainInjector(), web interface |
| Go version | Any | Any | Any | 1.18+ (generics) |
| Learning curve | None | Medium | High | Low |
Quick Comparison: Same App, Four Ways
The dependency graph: Config -> Database -> UserStore -> UserService -> API
Manual:
cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// No automatic shutdown, health checks, or lazy loadinggoogle/wire:
// wire.go — then run: wire ./...
func InitializeAPI() (*API, error) {
wire.Build(NewConfig, NewDatabase, NewUserStore, NewUserService, NewAPI)
return nil, nil
}
// No lifecycle hooks (OnStart/OnStop) or health checks; cleanup via returned func() from providersuber-go/fx:
app := fx.New(
fx.Provide(NewConfig, NewDatabase, NewUserStore, NewUserService),
fx.Invoke(func(api *API) { api.Run() }),
)
app.Run() // manages lifecycle, but reflection-basedsamber/do:
i := do.New()
do.Provide(i, NewConfig)
do.Provide(i, NewDatabase) // auto shutdown + health check
do.Provide(i, NewUserStore)
do.Provide(i, NewUserService)
api := do.MustInvoke[*API](i)
api.Run()
// defer i.Shutdown() — handles all cleanup automaticallyTesting with DI
DI makes testing straightforward — inject mocks instead of real implementations:
// Define a mock
type MockUserStore struct {
users map[string]*User
}
func (m *MockUserStore) FindByID(ctx context.Context, id string) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
// Test with manual injection
func TestUserService_GetUser(t *testing.T) {
mock := &MockUserStore{
users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
}
svc := NewUserService(mock, nil, slog.Default())
user, err := svc.GetUser(context.Background(), "1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got %q, want %q", user.Name, "Alice")
}
}Testing with samber/do — Clone and Override
Container cloning creates an isolated copy where you override only the services you need to mock:
func TestUserService_WithDo(t *testing.T) {
// Create a test injector with mock implementation
testInjector := do.New()
// Provide the mock UserStore interface
do.OverrideValue[UserStore](testInjector, &MockUserStore{
users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
})
// Provide other real services as needed
do.Provide[*slog.Logger](testInjector, func(i *do.Injector) (*slog.Logger, error) {
return slog.Default(), nil
})
svc := do.MustInvoke[*UserService](testInjector)
user, err := svc.GetUser(context.Background(), "1")
// ... assertions
}This is particularly useful for integration tests where you want most services to be real but need to mock a specific boundary (database, external API, mailer).
When to Adopt a DI Library
| Signal | Action |
|---|---|
| < 10 services, simple dependencies | Stay with manual constructor injection |
| 10-20 services, some cross-cutting concerns | Consider a DI library |
| 20+ services, lifecycle management needed | Strongly recommended |
| Need health checks, graceful shutdown | Use a library with built-in lifecycle support |
| Team unfamiliar with DI concepts | Start manual, migrate incrementally |
Common Mistakes
| Mistake | Fix |
|---|---|
| Global variables as dependencies | Pass through constructors or DI container |
init() for service setup | Explicit initialization in main() or container |
| Depending on concrete types | Accept interfaces at consumption boundaries |
| Passing the container everywhere (service locator) | Inject specific dependencies, not the container |
| Deep dependency chains (A->B->C->D->E) | Flatten — most services should depend on repositories and config directly |
| Creating a new container per request | One container per application; use scopes for request-level isolation |
Cross-References
- → See
samber/cc-skills-golang@golang-samber-doskill for detailed samber/do usage patterns - → See
samber/cc-skills-golang@golang-structs-interfacesskill for interface design and composition - → See
samber/cc-skills-golang@golang-testingskill for testing with dependency injection - → See
samber/cc-skills-golang@golang-project-layoutskill for DI initialization placement
References
[
{
"id": 1,
"name": "constructor-injection-not-globals",
"description": "Tests that dependencies are injected via constructors, not global variables or init()",
"prompt": "I have a UserService that needs a database connection and a logger. What's the best way to set this up in Go? I was thinking of using a package-level var for the database.",
"trap": "Without the skill, the model may accept the global variable approach or use init() for setup. The skill explicitly forbids globals and init() for service setup.",
"assertions": [
{"id": "1.1", "text": "Uses constructor injection (NewUserService taking dependencies as parameters)"},
{"id": "1.2", "text": "Explicitly advises against package-level variables for service dependencies"},
{"id": "1.3", "text": "Explains why globals are problematic (untestable, hidden dependencies, or coupling)"},
{"id": "1.4", "text": "Does NOT use init() for service initialization"},
{"id": "1.5", "text": "Returns a concrete struct pointer from the constructor, not an interface"}
]
},
{
"id": 2,
"name": "interface-defined-at-consumer",
"description": "Tests that interfaces are defined where consumed, not where implemented",
"prompt": "I'm building a Go service that uses a UserStore. Should I define the UserStore interface in the same package as the PostgreSQL implementation or somewhere else? Show me the correct pattern.",
"trap": "Without the skill, the model often defines the interface in the implementation package (next to the struct). The skill requires interfaces to be defined at the consumption site.",
"assertions": [
{"id": "2.1", "text": "Defines the interface in the consuming package (e.g. service package), not the implementation package"},
{"id": "2.2", "text": "Explains the principle: accept interfaces, return structs"},
{"id": "2.3", "text": "The implementation package returns a concrete struct pointer"},
{"id": "2.4", "text": "The consumer depends on its own locally-defined interface"},
{"id": "2.5", "text": "Does NOT have the implementation package import the consumer's interface"}
]
},
{
"id": 3,
"name": "container-not-passed-as-dependency",
"description": "Tests that the DI container is never passed as a dependency (service locator anti-pattern)",
"prompt": "I'm using samber/do for DI in my Go project. My UserService needs a Database and a Mailer. Should I pass the do.Injector to UserService so it can look up what it needs?",
"trap": "Without the skill, the model may accept passing the injector/container as a dependency. The skill explicitly forbids this as the service locator anti-pattern.",
"assertions": [
{"id": "3.1", "text": "Advises against passing the injector/container as a dependency"},
{"id": "3.2", "text": "Identifies this as the service locator anti-pattern"},
{"id": "3.3", "text": "Shows that the Injector should only exist at the composition root (main or app startup)"},
{"id": "3.4", "text": "Shows UserService receiving Database and Mailer directly as constructor parameters"},
{"id": "3.5", "text": "Shows the provider function using do.MustInvoke inside the provider, not inside UserService methods"}
]
},
{
"id": 4,
"name": "manual-di-for-small-projects",
"description": "Tests that small projects use manual DI, not a library",
"prompt": "I'm building a small Go REST API with about 5 services: config, database, user repository, user service, and HTTP handler. What DI approach should I use?",
"trap": "Without the skill, the model may recommend a DI library like Wire or Fx for any project. The skill says small projects (< 10 services) should use manual constructor injection.",
"assertions": [
{"id": "4.1", "text": "Recommends manual constructor injection for a project with only 5 services"},
{"id": "4.2", "text": "Does NOT recommend a DI library as the primary approach"},
{"id": "4.3", "text": "Shows wiring in main() with explicit constructor calls in dependency order"},
{"id": "4.4", "text": "Initializes infrastructure first, then repositories, then services, then transport"},
{"id": "4.5", "text": "Mentions that a DI library becomes worthwhile at 10-20+ services"}
]
},
{
"id": 5,
"name": "di-library-selection-judgment",
"description": "Tests correct DI library recommendation based on project characteristics",
"prompt": "I'm building a large Go microservice with 40+ services, complex lifecycle management (health checks, graceful shutdown), and I want compile-time type safety. My team is comfortable with Go generics. Which DI library should I use?",
"trap": "Without the skill, the model may recommend uber-go/fx (most popular) or google/wire. The skill's decision table shows samber/do matches all these criteria: any size, compile-time generics, built-in lifecycle, health checks, shutdown.",
"assertions": [
{"id": "5.1", "text": "Recommends samber/do as a strong fit given the criteria (generics, lifecycle, compile-time safety)"},
{"id": "5.2", "text": "Explains why uber-go/fx is a valid alternative but uses reflection (runtime errors, not compile-time)"},
{"id": "5.3", "text": "Explains why google/wire lacks built-in lifecycle management (no health checks, no shutdown)"},
{"id": "5.4", "text": "Mentions that samber/do requires Go 1.18+ for generics"},
{"id": "5.5", "text": "Discusses at least 3 DI library options from the decision table"}
]
},
{
"id": 6,
"name": "wire-build-constraint-and-codegen",
"description": "Tests proper google/wire setup with wireinject build constraint",
"prompt": "Set up google/wire for a Go application with Config, Database, UserStore, and UserService. Show the wire.go file and explain what happens when I run wire.",
"trap": "Without the skill, the model may forget the //go:build wireinject build constraint or not explain that wire.Build generates plain Go constructors.",
"assertions": [
{"id": "6.1", "text": "Includes //go:build wireinject build constraint in the wire.go file"},
{"id": "6.2", "text": "Uses wire.Build with all provider functions listed"},
{"id": "6.3", "text": "Shows wire.Bind for binding interface to implementation"},
{"id": "6.4", "text": "Explains that wire generates wire_gen.go with plain constructor calls"},
{"id": "6.5", "text": "Mentions that wire_gen.go must not be edited manually"}
]
},
{
"id": 7,
"name": "fx-lifecycle-hooks-pattern",
"description": "Tests proper uber-go/fx lifecycle hook usage for startup/shutdown",
"prompt": "Set up a database connection using uber-go/fx that connects on startup and cleanly closes on shutdown.",
"trap": "Without the skill, the model may open and close the connection manually instead of using fx.Lifecycle hooks. The skill requires OnStart/OnStop hooks.",
"assertions": [
{"id": "7.1", "text": "Uses fx.Lifecycle parameter in the provider function"},
{"id": "7.2", "text": "Registers OnStart hook for establishing the database connection"},
{"id": "7.3", "text": "Registers OnStop hook for closing the database connection"},
{"id": "7.4", "text": "Uses lc.Append(fx.Hook{...}) pattern"},
{"id": "7.5", "text": "OnStart and OnStop take context.Context as parameter"}
]
},
{
"id": 8,
"name": "testing-with-di-mock-injection",
"description": "Tests that DI enables testing by injecting mocks at the interface boundary",
"prompt": "Write a test for a UserService that depends on a UserStore interface. The test should verify that GetUser returns the correct user when found and an error when not found. Don't use any DI library.",
"trap": "Without the skill, the model may test with a real database or skip the mock pattern. The skill requires mocking at the interface boundary.",
"assertions": [
{"id": "8.1", "text": "Creates a mock implementation of the UserStore interface"},
{"id": "8.2", "text": "Injects the mock into UserService via the constructor (NewUserService)"},
{"id": "8.3", "text": "Tests both the success path (user found) and the error path (not found)"},
{"id": "8.4", "text": "Does NOT use a real database connection in the test"},
{"id": "8.5", "text": "The mock is defined in the test file, not as a package-level or global variable"}
]
},
{
"id": 9,
"name": "shallow-dependency-graph",
"description": "Tests that deep dependency chains are flagged as a design problem",
"prompt": "My Go application has this dependency chain: Config -> Database -> UserRepo -> UserService -> NotificationService -> OrderService -> PaymentService -> APIHandler. Is this a good architecture?",
"trap": "Without the skill, the model may accept this deep chain as normal layered architecture. The skill says deep chains signal design problems and recommends flattening.",
"assertions": [
{"id": "9.1", "text": "Identifies the deep dependency chain as a design problem"},
{"id": "9.2", "text": "Recommends flattening the dependency graph"},
{"id": "9.3", "text": "Suggests that most services should depend on repositories and config directly, not transitively through other services"},
{"id": "9.4", "text": "Explains the negative consequences of deep chains (fragility, hard to test, or hard to maintain)"},
{"id": "9.5", "text": "Proposes a concrete restructuring where OrderService and PaymentService don't depend on each other transitively"}
]
},
{
"id": 10,
"name": "one-container-per-app-not-per-request",
"description": "Tests that a DI container is created once per application, not per request",
"prompt": "I'm building a Go HTTP API with samber/do. In each HTTP handler, I'm creating a new do.New() injector, providing all services, then invoking the one I need. This way each request gets fresh services. Is this correct?",
"trap": "Without the skill, the model may accept the per-request container pattern as reasonable isolation. The skill explicitly forbids creating a new container per request.",
"assertions": [
{"id": "10.1", "text": "Identifies creating a new container per request as a mistake"},
{"id": "10.2", "text": "Recommends one container per application created at startup"},
{"id": "10.3", "text": "Explains the performance or correctness problem with per-request containers (recreating singletons, no connection reuse)"},
{"id": "10.4", "text": "Suggests using scopes for request-level isolation if needed"},
{"id": "10.5", "text": "Shows the container being created once in main() and services injected into handlers"}
]
},
{
"id": 11,
"name": "lazy-vs-eager-initialization",
"description": "Tests knowledge of lazy initialization preference and singleton vs transient distinction",
"prompt": "When using a DI container in Go, should all my services be created at application startup? I have a database pool, a cache client, and some request processing services.",
"trap": "Without the skill, the model may recommend eager initialization for everything. The skill prefers lazy initialization and distinguishes singletons for stateful services from transients for stateless ones.",
"assertions": [
{"id": "11.1", "text": "Recommends lazy initialization (services created on first use, not all at startup)"},
{"id": "11.2", "text": "Recommends singletons for stateful services like database connections and cache clients"},
{"id": "11.3", "text": "Recommends transients (or factories) for stateless request processing services"},
{"id": "11.4", "text": "Explains why lazy loading is beneficial (unused services are never created, faster startup)"},
{"id": "11.5", "text": "Notes which DI libraries support lazy loading (samber/do, fx) vs which don't (wire is all eager)"}
]
}
]
google/wire — Compile-Time Code Generation
Wire uses code generation to resolve the dependency graph at compile time. Type-safe, but requires a build step.
- Docs: github.com/google/wire | User Guide
Before writing Wire code, refer to the library's official documentation for up-to-date API signatures and examples.
Provider Definitions
// providers.go
package wire
import "github.com/google/wire"
// ProviderSet groups related providers
var InfraSet = wire.NewSet(
NewConfig,
NewDatabase,
NewCache,
)
var ServiceSet = wire.NewSet(
NewUserService,
wire.Bind(new(UserStore), new(*PostgresUserStore)), // bind interface to impl
)Injector Definition
// wire.go — build constraint ensures this is only used by the wire tool
//go:build wireinject
package main
import "github.com/google/wire"
func InitializeApp() (*App, error) {
wire.Build(
InfraSet,
ServiceSet,
NewApp,
)
return nil, nil // wire replaces this body
}Generated Code
Run wire ./... to produce wire_gen.go:
// wire_gen.go — DO NOT EDIT (auto-generated by wire)
func InitializeApp() (*App, error) {
config := NewConfig()
database, err := NewDatabase(config)
if err != nil {
return nil, err
}
cache := NewCache(config)
store := NewPostgresUserStore(database)
userService := NewUserService(store, cache)
app := NewApp(userService)
return app, nil
}Testing
Wire generates plain constructors, so testing uses manual injection — no container to clone:
func TestUserService(t *testing.T) {
mock := &MockUserStore{...}
svc := NewUserService(mock, NewTestCache())
// ... test
}Tradeoffs
- Errors caught at compile time (codegen fails if graph is incomplete)
- Requires running
wire ./...after every dependency change - No lazy loading — all dependencies created eagerly
- No built-in lifecycle management (health checks, shutdown)
- No runtime container — wire generates plain Go constructor calls
- Interface bindings require explicit
wire.Binddeclarations - Generated files (
wire_gen.go) must be committed and kept in sync
Wire injectors MUST use //go:build wireinject build constraint. Generated wire_gen.go MUST NOT be edited manually — always regenerate with wire ./....
Manual Constructor Injection
Manual DI is the simplest approach — pass dependencies through constructors. No library, no magic.
Complete Application Example
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// Layer 1: Configuration
cfg := LoadConfig()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Layer 2: Infrastructure
db, err := postgres.Connect(cfg.DatabaseURL)
if err != nil {
logger.Error("database connection failed", "error", err)
os.Exit(1)
}
defer db.Close()
cache := redis.NewClient(cfg.RedisURL)
defer cache.Close()
mailer := smtp.NewMailer(cfg.SMTPAddr)
// Layer 3: Repositories
userRepo := postgres.NewUserRepository(db)
orderRepo := postgres.NewOrderRepository(db)
// Layer 4: Services
userSvc := service.NewUserService(userRepo, cache, mailer, logger)
orderSvc := service.NewOrderService(orderRepo, userSvc, logger)
paymentSvc := service.NewPaymentService(orderRepo, cfg.StripeKey, logger)
// Layer 5: Transport
handler := http.NewHandler(userSvc, orderSvc, paymentSvc, logger)
server := http.NewServer(cfg.Port, handler)
// Run
go server.ListenAndServe()
<-ctx.Done()
server.Shutdown(context.Background())
}When Manual DI Works Well
- Small to medium projects (< 15 services)
- Simple dependency graph with clear layering
- No need for lazy loading or lifecycle management
- Team prefers explicit, visible wiring
When Manual DI Breaks Down
- Adding a new service means editing
main()and getting the wiring order right - Lifecycle management (health checks, graceful shutdown) must be hand-coded with
defer - No lazy initialization — all services are created at startup, even if unused
- Cross-cutting concerns (logging, tracing) must be threaded through every constructor
- With 30+ services, the wiring code becomes fragile and hard to maintain
Manual DI SHOULD be the default for small projects (< 15 services). Dependencies MUST be initialized in order — infrastructure first, then repositories, then services, then transport.
samber/do — Generics-Based DI
For the full samber/do API, patterns, and advanced features, see the `samber/cc-skills-golang@golang-samber-do` skill.
Type-safe dependency injection using Go generics. No reflection, no code generation, simple API.
- Docs: do.samber.dev | github.com/samber/do/v2
Core Pattern
// Register services with providers
injector := do.New()
do.Provide(injector, func(i do.Injector) (*UserService, error) {
db := do.MustInvoke[*Database](i)
return NewUserService(db), nil
})
// Invoke services (lazy — created on demand)
svc := do.MustInvoke[*UserService](injector)
// Graceful shutdown — all services implementing Shutdowner are closed
injector.ShutdownOnSignalsWithContext(ctx, os.Interrupt)Why samber/do
- No code generation — no build step, no generated files to maintain
- No reflection — errors are caught at compile time via generics, not at runtime
- Strongly typed — Go generics provide full type safety without
interface{}casts - Built-in lifecycle — health checks and graceful shutdown detected automatically
- Container cloning — create isolated test containers from production configuration
- Simple API —
Provide,Invoke,Shutdown— that's most of what you need - Package system — organize services by domain without manual wiring order
→ See samber/cc-skills-golang@golang-samber-do for full application setup, package organization, lifecycle management, debugging, testing with clone + override, and complete API reference.
uber-go/dig + uber-go/fx — Reflection-Based DI
dig is the low-level DI container; fx is the full application framework built on top. Powerful but uses reflection — errors appear at startup, not compile time.
Before writing dig/fx code, refer to the library's official documentation for up-to-date API signatures and examples.
dig — Basic Container
func main() {
container := dig.New()
container.Provide(NewConfig)
container.Provide(NewDatabase)
container.Provide(NewUserStore)
container.Provide(NewUserService)
// Invoke — dig resolves the full dependency chain
err := container.Invoke(func(svc *UserService) {
svc.Run()
})
if err != nil {
log.Fatal(err)
}
}Named Dependencies
type DatabaseParams struct {
dig.In
Primary *sql.DB `name:"primary"`
Replica *sql.DB `name:"replica"`
}
container.Provide(NewPrimaryDB, dig.Name("primary"))
container.Provide(NewReplicaDB, dig.Name("replica"))
container.Provide(func(p DatabaseParams) *UserService {
return &UserService{
writer: p.Primary,
reader: p.Replica,
}
})dig Tradeoffs
- Uses reflection — type mismatches are runtime errors, not compile errors
dig.Inanddig.Outstructs add boilerplate for complex graphs- No built-in lifecycle management
- Powerful grouping with
dig.Groupfor collecting multiple implementations
fx — Full Application Framework
Basic Application
func main() {
app := fx.New(
fx.Provide(
NewConfig,
NewDatabase,
NewUserStore,
NewUserService,
),
fx.Invoke(RegisterRoutes),
fx.Invoke(StartServer),
)
app.Run() // blocks until signal, then calls shutdown hooks
}Lifecycle Hooks
func NewDatabase(lc fx.Lifecycle, cfg *Config) (*Database, error) {
db := &Database{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return db.Connect(cfg.URL)
},
OnStop: func(ctx context.Context) error {
return db.Close()
},
})
return db, nil
}Modules
var InfraModule = fx.Module("infra",
fx.Provide(NewConfig),
fx.Provide(NewDatabase),
fx.Provide(NewCache),
)
var ServiceModule = fx.Module("service",
fx.Provide(NewUserService),
fx.Provide(NewOrderService),
)
app := fx.New(InfraModule, ServiceModule, fx.Invoke(StartServer))Testing with fx
func TestUserService(t *testing.T) {
var svc *UserService
app := fxtest.New(t,
fx.Provide(NewMockUserStore),
fx.Provide(NewUserService),
fx.Populate(&svc),
)
app.RequireStart()
defer app.RequireStop()
// ... test svc
}fx Tradeoffs
- Full application framework — manages startup, shutdown, and signal handling
- Reflection-based — errors at startup, not compile time
- Steep learning curve —
fx.In,fx.Out,fx.Annotate,fx.Decorate - Built-in lifecycle (OnStart/OnStop hooks)
- Heavyweight — pulls in the full fx framework
fxtestpackage for testing, but requires starting/stopping the app
fx lifecycle hooks MUST be used for start/stop — register OnStart/OnStop via fx.Lifecycle. fx modules SHOULD group related providers — use fx.Module to organize by domain.
Related skills
How it compares
Apply after golang-structs-interfaces defines consumer interfaces; use before golang-grpc when wiring handler dependencies.
FAQ
Does golang-dependency-injection allow package-level database vars?
golang-dependency-injection forbids package-level database or logger variables and init setup for services. Dependencies must pass through constructors such as NewUserService(db, logger) so tests can inject fakes.
Which DI style does golang-dependency-injection require?
golang-dependency-injection requires explicit constructor injection in Go without globals, init hooks, or hidden singletons. Agents must generate New functions accepting interfaces or concrete dependencies as parameters.
Is Golang Dependency Injection safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.