feat: add golang-dependency-injection skill and supporting documentation

This commit is contained in:
2026-04-02 10:14:19 +07:00
parent 43cb6463b3
commit 5a40bae269
3 changed files with 379 additions and 0 deletions
@@ -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.