feat: add golang-dependency-injection skill and supporting documentation
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# 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](https://github.com/google/wire) | [User Guide](https://github.com/google/wire/blob/main/docs/guide.md)
|
||||
|
||||
Before writing Wire code, refer to the library's official documentation for up-to-date API signatures and examples.
|
||||
|
||||
## Provider Definitions
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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`:
|
||||
|
||||
```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:
|
||||
|
||||
```go
|
||||
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.Bind` declarations
|
||||
- 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 ./...`.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Manual Constructor Injection
|
||||
|
||||
Manual DI is the simplest approach — pass dependencies through constructors. No library, no magic.
|
||||
|
||||
## Complete Application Example
|
||||
|
||||
```go
|
||||
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.
|
||||
Reference in New Issue
Block a user