mirror of
https://github.com/samber/cc-skills-golang.git
synced 2026-09-11 19:46:44 +03:00
feat: add golang-uber-dig and golang-uber-fx skills (#25)
* feat: add golang-uber-dig and golang-uber-fx skills Two new library skills covering uber-go's reflection-based DI ecosystem. golang-uber-dig covers the container, Provide/Invoke, dig.In/dig.Out, named values, value groups, dig.As, optional deps, Decorate, Scopes, error handling, and Visualize. golang-uber-fx covers fx.New/Run, lifecycle hooks, fx.Module, fx.Annotate, fx.Supply/Replace/Decorate, fxevent logging, and fxtest. Each skill ships recipes.md (end-to-end app examples) and testing.md (test patterns and CI graph validation), and cross-references golang-samber-do, golang-google-wire, and golang-dependency-injection. * refactor(uber-dig,uber-fx): trim SKILL.md under 2,500 tokens Both SKILL.md exceeded the project budget. Moved Decorate, Scopes, optional deps, error helpers, Visualize, and Quick Reference into references/advanced.md for both skills. dig: 3,744 -> 2,264 tok. fx: 4,466 -> 2,499 tok. README updated. * test(uber-dig,uber-fx): add eval prompts and assertions 11 adversarial evals per skill targeting unique guidance: parameter objects, value groups (with flatten), named values, dig.As to hide concrete types, scopes for request locals, container at composition root, DryRun graph validation, Decorate, RecoverFromPanics, fx vs dig choice. fx evals additionally cover lifecycle non-blocking OnStart, fx.Annotate vs fx.Out, modules, fx.Supply, fx.Replace + fx.Populate in fxtest, fxevent.ZapLogger, manual lifecycle for CLI embedding. * test(uber-dig,uber-fx): add preliminary eval results to EVALUATIONS.md Ran 4 evals × 2 configs per skill (16 subagents total) with Claude Opus 4.7. Both skills score 100% with-skill. Without-skill: dig 90% (-10pp uplift), fx 95% (-5pp uplift). The base model has very strong baseline knowledge of both libraries; only adversarial evals targeting subtle API choices (Decorate vs scope-shadow Provide; fx.As interface binding) showed meaningful uplift. Full 11-eval suite (53/56 assertions) remains in evals.json for re-runs via /skill-creator. * fix(uber-dig): clean up unused context import and fix handler signature in recipes * fix(golang-uber-dig,golang-uber-fx): address PR #25 review comments and deep review findings PR comments: - dig/advanced.md: rephrase "module boundaries" to "scope/package wiring boundaries" - dig/recipes.md: fix ignored repo.List error in HTTP handler (return 500) - dig/recipes.md: remove unused context import and _ = context.Background hack - dig/recipes.md: fix handle() signature to idiomatic (w, r) order - fx/testing.md: assert net.Listen error before using listener Deep review: - dig/advanced.md: add code example for dig.Export(true) call site - dig/advanced.md: clarify c.String() as text summary, not DOT output - dig/recipes.md: add missing root.Provide(NewHandler) to request-scope recipe - fx/advanced.md: add fx.ErrorHook to lifecycle quick reference - fx/testing.md: add zaptest/observer import hint to observer example - fx/testing.md: comment why lc.Start is used instead of RequireStart
This commit is contained in:
+92
-1
@@ -47,8 +47,10 @@
|
||||
| `golang-modernize` | v1.0.0 | 76 | 95% | 34% | +61pp | 2.79× | |
|
||||
| `golang-samber-slog` | v1.0.0 | 62 | 92% | **73%** | +19pp | 1.26× | **Low delta, high without** |
|
||||
| `golang-samber-lo` | v1.0.0 | 86 | 97% | 57% | +40pp | 1.70× | |
|
||||
| `golang-uber-fx` | v1.0.0 | 21 | 100% | **95%** | +5pp | 1.05× | **Low delta, high without** |
|
||||
| `golang-uber-dig` | v1.0.0 | 20 | 100% | **90%** | +10pp | 1.11× | **Low delta, high without** |
|
||||
| `golang-samber-do` | v1.0.0 | 53 | 100% | 19% | +81pp | 5.26× | |
|
||||
| **Total (35 skills)** | | **3141** | **98%** | **54%** | **+44pp** | **1.81×** | |
|
||||
| **Total (37 skills)** | | **3182** | **98%** | **55%** | **+43pp** | **1.78×** | |
|
||||
|
||||
## `golang-naming` — v1.0.0
|
||||
|
||||
@@ -4410,4 +4412,93 @@
|
||||
|
||||
</details>
|
||||
|
||||
## `golang-uber-dig` — v1.0.0
|
||||
|
||||
| | With Skill | Without Skill | Delta |
|
||||
| ----------- | ------------------ | ------------------- | --------- |
|
||||
| **Overall** | **20/20 (100%)** | **18/20 (90%)** | **+10pp** |
|
||||
|
||||
<details>
|
||||
<summary>Full breakdown (20 assertions)</summary>
|
||||
|
||||
**Model:** Claude Opus 4.7 | **Runs:** 4 evals × 2 configs = 8 subagents | **Grading:** human (assertion-by-assertion)
|
||||
|
||||
> **Note:** Preliminary subset of the 11-eval suite in `skills/golang-uber-dig/evals/evals.json` (53 assertions total). The remaining 7 evals cover named values, dig.As, scopes for request-locals, DryRun graph validation, RecoverFromPanics, group flatten, and the fx-vs-dig recommendation. Re-run the full suite via `/skill-creator` for a complete report.
|
||||
|
||||
| # | Assertion | With | Without |
|
||||
| ---- | -------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------ |
|
||||
| | **1. param-objects-many-deps** — dig.In for 4+ deps | **<span class="g">5/5</span>** | **<span class="g">5/5</span>** |
|
||||
| 1.1 | Embeds dig.In in the parameter struct | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.2 | Constructor takes the params struct as a single argument | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.3 | Does NOT keep the long parameter list | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.4 | Does NOT use a plain struct without dig.In | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.5 | Mentions readability/maintainability benefit | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| | **2. value-groups-for-handlers** — group:"routes" | **<span class="g">5/5</span>** | **<span class="g">5/5</span>** |
|
||||
| 2.1 | Each handler returns dig.Out tagged group:"routes" | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 2.2 | NewRouter consumes dig.In with []slice tagged group:"routes" | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 2.3 | Handlers added with c.Provide; no manual slice in main() | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 2.4 | Does NOT manually assemble the handler slice | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 2.5 | Mentions group order is not guaranteed | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| | **6. container-not-passed-around** — composition-root only | **<span class="g">5/5</span>** | **<span class="g">5/5</span>** |
|
||||
| 6.1 | Advises against passing *dig.Container into business code | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 6.2 | Container only at composition root (main / startup) | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 6.3 | UserHandler takes typed dependencies as constructor parameters | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 6.4 | Mentions service locator anti-pattern OR explains downside | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 6.5 | Does NOT show example with container injected into handler | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| | **8. decorate-not-rewrite** — Decorate vs scope-shadow Provide | **<span class="g">5/5</span>** | **<span class="r">3/5</span>** |
|
||||
| 8.1 | Uses Decorate (c.Decorate / scope.Decorate) on *zap.Logger | <span class="g">✓</span> | <span class="r">✗</span> uses scope.Provide override |
|
||||
| 8.2 | Decorator returns log.Named("worker") | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 8.3 | Decorate at the worker scope/module, not globally | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 8.4 | Does NOT modify the original NewLogger constructor | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 8.5 | Mentions decorator scope semantics (applies to scope and descendants) | <span class="g">✓</span> | <span class="r">✗</span> describes scope-override semantics instead |
|
||||
|
||||
**Analyst pass:** evals 1, 2, and 6 score 5/5 in both configurations — they test knowledge the base model already has, hitting the "common knowledge" anti-pattern flagged in `CLAUDE.md`. Only eval 8 differentiates the skill: the without-skill agent reaches for `scope.Provide` shadowing (which would create a circular dependency at resolution time), while the skill steers to the correct `Decorate` API. Future iterations should redesign evals 1, 2, 6 to target subtler guidance and add cases the model gets wrong without the skill.
|
||||
|
||||
</details>
|
||||
|
||||
## `golang-uber-fx` — v1.0.0
|
||||
|
||||
| | With Skill | Without Skill | Delta |
|
||||
| ----------- | ------------------ | ------------------- | --------- |
|
||||
| **Overall** | **21/21 (100%)** | **20/21 (95%)** | **+5pp** |
|
||||
|
||||
<details>
|
||||
<summary>Full breakdown (21 assertions)</summary>
|
||||
|
||||
**Model:** Claude Opus 4.7 | **Runs:** 4 evals × 2 configs = 8 subagents | **Grading:** human (assertion-by-assertion)
|
||||
|
||||
> **Note:** Preliminary subset of the 11-eval suite in `skills/golang-uber-fx/evals/evals.json` (56 assertions total). The remaining 7 evals cover fx.Annotate vs fx.Out, fx.Module organization, fx.Supply, value groups, fxevent.ZapLogger, manual lifecycle for CLI embedding, and the fx-vs-dig recommendation. Re-run the full suite via `/skill-creator` for a complete report.
|
||||
|
||||
| # | Assertion | With | Without |
|
||||
| ---- | -------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------ |
|
||||
| | **1. lifecycle-not-init** — OnStart with goroutine | **<span class="g">6/6</span>** | **<span class="g">6/6</span>** |
|
||||
| 1.1 | Injects fx.Lifecycle into NewHTTPServer | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.2 | lc.Append with fx.Hook (OnStart starts server, OnStop calls Shutdown) | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.3 | OnStart launches srv.Serve in a goroutine | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.4 | Does NOT call srv.Serve directly inside the constructor | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.5 | Does NOT use init() to start the server | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 1.6 | OnStop calls srv.Shutdown(ctx) for graceful shutdown | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| | **5. fxtest-with-populate** — fxtest.New + fx.Populate | **<span class="g">5/5</span>** | **<span class="g">5/5</span>** |
|
||||
| 5.1 | Uses fxtest.New(t, ...) instead of fx.New | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 5.2 | Uses fx.Populate(&svc) to extract *UserService | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 5.3 | Calls app.RequireStart() and app.RequireStop() | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 5.4 | Provides a fake Database (interface, not real DB) | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 5.5 | Does NOT use fx.Invoke as the primary extraction mechanism | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| | **6. replace-for-fakes** — fx.Replace with fx.Annotate(fx.As) | **<span class="g">5/5</span>** | **<span class="r">4/5</span>** |
|
||||
| 6.1 | Uses fx.Replace (or fx.Decorate) inside fxtest.New | <span class="g">✓</span> | <span class="g">✓</span> uses fx.Decorate |
|
||||
| 6.2 | Composes ProductionModule alongside the override; module unchanged | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 6.3 | Uses fx.Annotate with fx.As(new(Database)) for interface binding | <span class="g">✓</span> | <span class="r">✗</span> mentions but does not use |
|
||||
| 6.4 | Does NOT modify or duplicate the production module | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 6.5 | Mentions Replace/Decorate is appropriate for tests | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| | **10. onstart-non-blocking** — long-running work in goroutine | **<span class="g">5/5</span>** | **<span class="g">5/5</span>** |
|
||||
| 10.1 | OnStart launches the long-running method in a goroutine | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 10.2 | OnStart returns nil quickly without waiting | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 10.3 | OnStop signals stop and waits for drain (with timeout) | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 10.4 | Does NOT call the long-running method synchronously in OnStart | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
| 10.5 | Mentions a blocking OnStart hangs the boot | <span class="g">✓</span> | <span class="g">✓</span> |
|
||||
|
||||
**Analyst pass:** 3 of 4 evals score equally with and without the skill — the model's baseline knowledge of fx is very strong (lifecycle hooks, fxtest.New, OnStart/goroutine pattern). Only eval 6 differentiates: without the skill the agent picks `fx.Decorate` and skips the `fx.As` interface binding that the production graph requires. This is consistent with a well-known framework where the skill mainly adds value on subtle API choices. Future iterations should target less common patterns: fx.Annotate vs fx.Out trade-offs, fx.Module decorator scoping, fxevent customization, manual lifecycle for CLI embedding.
|
||||
|
||||
</details>
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@@ -185,8 +185,8 @@ These skills are designed as **atomic, cross-referencing units**. A skill may re
|
||||
| ❌ `golang-spf13-cobra` | | — | 0 | 0 | 0 |
|
||||
| ❌ `golang-spf13-viper` | | — | 0 | 0 | 0 |
|
||||
| ❌ `golang-swagger` | | — | 0 | 0 | 0 |
|
||||
| ❌ `golang-uber-dig` | | — | 0 | 0 | 0 |
|
||||
| ❌ `golang-uber-fx` | | — | 0 | 0 | 0 |
|
||||
| ✅ `golang-uber-dig` | ⚡ | -10% | 107 | 2,264 | 5,904 |
|
||||
| ✅ `golang-uber-fx` | ⚡ | -5% | 118 | 2,499 | 6,747 |
|
||||
| ✅ `golang-samber-do` | ⚡ | -81% | 70 | 1,746 | 3,269 |
|
||||
| ✅ `golang-samber-hot` | ⚡ | -54% | 118 | 1,843 | 7,273 |
|
||||
| ✅ `golang-samber-lo` | ⚡ | -40% | 155 | 2,410 | 10,031 |
|
||||
@@ -201,7 +201,7 @@ These skills are designed as **atomic, cross-referencing units**. A skill may re
|
||||
|
||||
| | With Skill | Without Skill | Delta |
|
||||
| ----------- | ------------------- | ------------------- | --------- |
|
||||
| **Overall** | **3065/3141 (98%)** | **1691/3141 (54%)** | **+44pp** |
|
||||
| **Overall** | **3106/3182 (98%)** | **1729/3182 (54%)** | **+44pp** |
|
||||
|
||||
See [EVALUATIONS.md](./EVALUATIONS.md) for the full per-skill breakdown.
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
name: golang-uber-dig
|
||||
description: "Implements dependency injection in Golang using uber-go/dig — reflection-based container, Provide/Invoke, dig.In/dig.Out parameter and result objects, named values, value groups, optional dependencies, scopes, and Decorate. Apply when using or adopting uber-go/dig, when the codebase imports `go.uber.org/dig`, or when wiring an application graph at startup. For higher-level lifecycle and modules, see `samber/cc-skills-golang@golang-uber-fx` skill."
|
||||
user-invocable: true
|
||||
license: MIT
|
||||
compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.
|
||||
metadata:
|
||||
author: samber
|
||||
version: "1.0.0"
|
||||
openclaw:
|
||||
emoji: "⛏️"
|
||||
homepage: https://github.com/samber/cc-skills-golang
|
||||
requires:
|
||||
bins:
|
||||
- go
|
||||
install: []
|
||||
skill-library-version: "1.19.0"
|
||||
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs
|
||||
---
|
||||
|
||||
**Persona:** You are a Go architect wiring an application graph with dig. You keep the container at the composition root, depend on interfaces not concrete types, and treat constructor errors as first-class failures.
|
||||
|
||||
# Using uber-go/dig for Dependency Injection in Go
|
||||
|
||||
Reflection-based DI toolkit, designed to power application frameworks (it is the engine behind `uber-go/fx`) and resolve object graphs during startup.
|
||||
|
||||
**Official Resources:**
|
||||
|
||||
- [pkg.go.dev/go.uber.org/dig](https://pkg.go.dev/go.uber.org/dig)
|
||||
- [github.com/uber-go/dig](https://github.com/uber-go/dig)
|
||||
|
||||
This skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform.
|
||||
|
||||
```bash
|
||||
go get go.uber.org/dig
|
||||
```
|
||||
|
||||
> **When to choose dig over fx.** Use raw dig only when you need the wiring graph and not fx's lifecycle, signal handling, or app boot semantics. For most production apps, prefer fx (`samber/cc-skills-golang@golang-uber-fx` skill) — it adds lifecycle hooks, modules, and signal-aware `Run()` on top of the same dig engine.
|
||||
|
||||
## Container
|
||||
|
||||
```go
|
||||
import "go.uber.org/dig"
|
||||
|
||||
c := dig.New()
|
||||
```
|
||||
|
||||
Useful options: `dig.DeferAcyclicVerification()` (faster startup), `dig.RecoverFromPanics()` (turn panics into `dig.PanicError`), `dig.DryRun(true)` (validate without invoking).
|
||||
|
||||
## Provide and Invoke
|
||||
|
||||
```go
|
||||
// Register a constructor — lazy, only runs when its output is needed
|
||||
err := c.Provide(func(cfg *Config) (*sql.DB, error) {
|
||||
return sql.Open("postgres", cfg.DSN)
|
||||
})
|
||||
|
||||
// Pull a service out of the container by asking for it as a function parameter
|
||||
err = c.Invoke(func(db *sql.DB) error {
|
||||
return db.Ping()
|
||||
})
|
||||
```
|
||||
|
||||
Constructors are **lazy** and **memoized**: each output type is built once and shared (singleton per container). `Provide` errors at registration if the constructor is malformed; `Invoke` returns the constructor's error wrapped with the dependency path that triggered it.
|
||||
|
||||
A dig constructor is any function. Inputs are dependencies, outputs are provided types. `error` (last return) signals construction failure. Follow "accept interfaces, return structs".
|
||||
|
||||
## Parameter Objects with `dig.In`
|
||||
|
||||
Once a constructor has 4+ dependencies, embed `dig.In` to group them as struct fields and tag fields:
|
||||
|
||||
```go
|
||||
type HandlerParams struct {
|
||||
dig.In
|
||||
|
||||
Logger *zap.Logger
|
||||
DB *sql.DB
|
||||
Cache *redis.Client `optional:"true"` // zero value if not provided
|
||||
DBRO *sql.DB `name:"readonly"` // named dependency
|
||||
Routes []http.Handler `group:"routes"` // value group
|
||||
}
|
||||
|
||||
func NewHandler(p HandlerParams) *Handler { /* ... */ }
|
||||
```
|
||||
|
||||
Tags: `name:"..."`, `optional:"true"`, `group:"..."`.
|
||||
|
||||
## Result Objects with `dig.Out`
|
||||
|
||||
Return several values from one constructor and attach `name`/`group` tags to results:
|
||||
|
||||
```go
|
||||
type ConnResult struct {
|
||||
dig.Out
|
||||
|
||||
ReadWrite *sql.DB `name:"primary"`
|
||||
ReadOnly *sql.DB `name:"readonly"`
|
||||
}
|
||||
|
||||
func NewConnections(cfg *Config) (ConnResult, error) { /* ... */ }
|
||||
```
|
||||
|
||||
## Named Values
|
||||
|
||||
Two providers of the same type collide. Disambiguate with `dig.Name`:
|
||||
|
||||
```go
|
||||
c.Provide(NewPrimaryDB, dig.Name("primary"))
|
||||
c.Provide(NewReadOnlyDB, dig.Name("readonly"))
|
||||
```
|
||||
|
||||
Consume by adding `name:"primary"` / `name:"readonly"` to a `dig.In` field.
|
||||
|
||||
## Value Groups
|
||||
|
||||
Many providers, one consumer slice — typical for HTTP handlers, health checks, migrations:
|
||||
|
||||
```go
|
||||
type RouteResult struct {
|
||||
dig.Out
|
||||
Handler http.Handler `group:"routes"`
|
||||
}
|
||||
|
||||
func NewUserHandler(db *sql.DB) RouteResult { /* ... */ }
|
||||
func NewPostHandler(db *sql.DB) RouteResult { /* ... */ }
|
||||
|
||||
type ServerParams struct {
|
||||
dig.In
|
||||
Routes []http.Handler `group:"routes"`
|
||||
}
|
||||
```
|
||||
|
||||
**Flatten** — append `,flatten` (e.g. `group:"routes,flatten"`) to unwrap a slice instead of nesting it. Group order is **not guaranteed**; if order matters, provide an explicit ordered slice from a single constructor.
|
||||
|
||||
## Provide as Interface (`dig.As`)
|
||||
|
||||
Register a concrete constructor and expose it under one or more interfaces without a separate adapter:
|
||||
|
||||
```go
|
||||
c.Provide(NewPostgresDB, dig.As(new(Database), new(io.Closer)))
|
||||
// Consumers ask for Database or io.Closer; *PostgresDB stays hidden.
|
||||
```
|
||||
|
||||
## Full Application Example
|
||||
|
||||
```go
|
||||
func main() {
|
||||
c := dig.New()
|
||||
|
||||
must(c.Provide(NewConfig))
|
||||
must(c.Provide(NewLogger))
|
||||
must(c.Provide(NewDatabase))
|
||||
must(c.Provide(NewServer))
|
||||
|
||||
err := c.Invoke(func(srv *http.Server) error {
|
||||
return srv.ListenAndServe()
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func must(err error) { if err != nil { panic(err) } }
|
||||
```
|
||||
|
||||
dig has **no built-in lifecycle**. If you need OnStart/OnStop hooks, signal handling, and graceful shutdown, use fx — see `samber/cc-skills-golang@golang-uber-fx` skill.
|
||||
|
||||
For Decorate, Scopes, optional deps, error helpers, and Visualize, see [advanced.md](./references/advanced.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Keep the container at the composition root — never pass `*dig.Container` as a parameter; treat it like a plumbing detail of `main()`. Service-locator patterns defeat the testability gains of DI.
|
||||
2. Depend on interfaces, not concrete types — lets you swap implementations in tests without touching production code, and lets you use `dig.As` to expose narrow interfaces from wide structs.
|
||||
3. Prefer parameter objects (`dig.In` structs) once a constructor has 4+ dependencies — call sites stay readable and adding a new dependency is a one-line change instead of a signature break.
|
||||
4. Group registration by module (one file per module that calls `c.Provide` for its types) — review and refactoring become a per-module concern, and you can extract a module into a fx.Module later without rewriting wiring.
|
||||
5. Validate the graph eagerly in tests — call `c.Invoke` against the composition root in CI to surface missing providers at boot time, not at first request. `DryRun(true)` skips constructor execution.
|
||||
6. Return errors from constructors instead of panicking — dig wraps them with the dependency path, which makes the failure point obvious.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Passing the container into services | The container belongs to `main()`. Inject the typed dependencies a service needs; otherwise tests need to build a real container. |
|
||||
| Two providers for the same type without `Name` | dig errors at `Provide` time. Either name them, or merge into a single provider that returns a `dig.Out` result struct. |
|
||||
| Ignoring `Provide` errors | Wrap each `Provide` with a `must` helper. A silent registration error becomes a missing-type error far later. |
|
||||
| Using groups when ordering matters | Groups are unordered. If order matters (middleware chain, migration sequence), provide an explicit ordered slice with one constructor. |
|
||||
| Constructors with side effects on import | Keep `init()` empty — start work only inside the constructor, after the graph is built. |
|
||||
|
||||
## Testing
|
||||
|
||||
dig containers are cheap — build a fresh one per test, override providers with `Decorate`, and call `Invoke` to drive the system. For full patterns (per-test wiring, shared helpers, graph validation in CI, asserting wire-time errors, recovering from constructor panics), see [testing.md](./references/testing.md).
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [advanced.md](./references/advanced.md) — Decorate, Scopes, optional deps, error helpers, Visualize, full Quick Reference
|
||||
- [recipes.md](./references/recipes.md) — end-to-end examples: HTTP server with route group, two databases, request scopes, decorators, dry-run validation
|
||||
- [testing.md](./references/testing.md) — testing patterns and graph validation
|
||||
|
||||
## Cross-References
|
||||
|
||||
- → See `samber/cc-skills-golang@golang-uber-fx` skill for application lifecycle, modules, and signal-aware Run() built on top of dig
|
||||
- → See `samber/cc-skills-golang@golang-dependency-injection` skill for DI concepts and library comparison
|
||||
- → See `samber/cc-skills-golang@golang-samber-do` skill for a generics-based alternative without reflection
|
||||
- → See `samber/cc-skills-golang@golang-google-wire` skill for compile-time DI (no runtime container)
|
||||
- → See `samber/cc-skills-golang@golang-structs-interfaces` skill for interface design patterns
|
||||
- → See `samber/cc-skills-golang@golang-testing` skill for general testing patterns
|
||||
|
||||
If you encounter a bug or unexpected behavior in uber-go/dig, open an issue at https://github.com/uber-go/dig/issues.
|
||||
@@ -0,0 +1,154 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "param-objects-many-deps",
|
||||
"description": "Tests use of dig.In parameter objects when a constructor has many dependencies",
|
||||
"prompt": "I'm wiring a Go service with uber-go/dig. I have a NewServer constructor that needs *zap.Logger, *sql.DB, *redis.Client, *Config, and *MetricsRegistry. The signature is getting unwieldy. How should I clean it up?",
|
||||
"trap": "Without the skill, the model keeps the long signature or wraps args in an ad-hoc struct without dig.In, missing the parameter-object pattern that lets the container fill the fields.",
|
||||
"assertions": [
|
||||
{"id": "1.1", "text": "Embeds dig.In in the parameter struct"},
|
||||
{"id": "1.2", "text": "Constructor takes the params struct as a single argument"},
|
||||
{"id": "1.3", "text": "Does NOT just keep the long parameter list as the answer"},
|
||||
{"id": "1.4", "text": "Does NOT use a plain struct without dig.In (which would not be filled by the container)"},
|
||||
{"id": "1.5", "text": "Mentions readability/maintainability benefit (adding a new dep is a one-line change)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "value-groups-for-handlers",
|
||||
"description": "Tests value groups when many constructors must contribute to one slice",
|
||||
"prompt": "In my Go HTTP server wired with uber-go/dig, I have several constructors (NewUserHandler, NewPostHandler, NewHealthHandler) and a NewRouter that should consume all of them. Each handler is registered independently. How do I wire this without the router knowing which handlers exist?",
|
||||
"trap": "Without the skill, the model invokes each handler individually inside main() and passes a slice to NewRouter, missing the group:\"...\" tag that decouples producers from consumers.",
|
||||
"assertions": [
|
||||
{"id": "2.1", "text": "Each handler constructor returns a dig.Out struct (or uses dig.Group via Provide option) tagged with group:\"routes\" (or similar group name)"},
|
||||
{"id": "2.2", "text": "NewRouter consumes a dig.In with a slice field tagged group:\"routes\""},
|
||||
{"id": "2.3", "text": "Handlers are added with c.Provide — no manual slice assembly in main()"},
|
||||
{"id": "2.4", "text": "Does NOT manually assemble the handler slice and pass it to NewRouter"},
|
||||
{"id": "2.5", "text": "Mentions that group order is not guaranteed (or is silent on it; does NOT claim a specific order is guaranteed)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "named-values-multiple-dbs",
|
||||
"description": "Tests dig.Name for multiple instances of the same type",
|
||||
"prompt": "My Go app needs two *sql.DB connections — one to the primary write database and one to a read replica. Both use the same *sql.DB type. Show me how to register and consume them with uber-go/dig.",
|
||||
"trap": "Without the skill, the model wraps the two connections in different types (struct PrimaryDB / struct ReadOnlyDB) instead of using dig.Name on a single type.",
|
||||
"assertions": [
|
||||
{"id": "3.1", "text": "Uses dig.Name(\"...\") on c.Provide for at least one of the two databases (or uses dig.Out result tags name:\"...\")"},
|
||||
{"id": "3.2", "text": "Both providers register the same *sql.DB type, distinguished by name"},
|
||||
{"id": "3.3", "text": "Consumer uses dig.In with name:\"primary\" / name:\"readonly\" tags"},
|
||||
{"id": "3.4", "text": "Does NOT introduce wrapper types like type PrimaryDB *sql.DB just to disambiguate"},
|
||||
{"id": "3.5", "text": "Does NOT register both as plain *sql.DB without names (which dig rejects at Provide time)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "as-to-hide-concrete",
|
||||
"description": "Tests dig.As to expose only an interface to consumers",
|
||||
"prompt": "I have a *PostgresDB concrete struct in my Go code that has many internal fields and methods. I want consumers to depend only on a Database interface (Query, Exec). How do I register it with uber-go/dig so consumers can never accidentally see the concrete type?",
|
||||
"trap": "Without the skill, the model registers the constructor returning Database (interface) directly, which works but loses type info; or writes a separate adapter constructor — missing dig.As that does this in one line.",
|
||||
"assertions": [
|
||||
{"id": "4.1", "text": "Uses dig.As(new(Database)) as a Provide option on the *PostgresDB constructor"},
|
||||
{"id": "4.2", "text": "The constructor itself returns the concrete *PostgresDB"},
|
||||
{"id": "4.3", "text": "Consumers ask for the Database interface, not *PostgresDB"},
|
||||
{"id": "4.4", "text": "Does NOT write a separate wrapper/adapter constructor that just returns the interface"},
|
||||
{"id": "4.5", "text": "Mentions or demonstrates that only the interface is exposed in the graph"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "scopes-for-request-locals",
|
||||
"description": "Tests scopes for per-request dependencies",
|
||||
"prompt": "In my Go web app wired with uber-go/dig, I have global services (logger, database) and per-request data (current user, request ID). How do I keep request-scoped values from leaking across concurrent requests?",
|
||||
"trap": "Without the skill, the model registers everything in the root container and uses context.Value for per-request data — missing dig.Scope which provides isolated child containers.",
|
||||
"assertions": [
|
||||
{"id": "5.1", "text": "Uses c.Scope(\"request\") (or root.Scope) to create a child container per request"},
|
||||
{"id": "5.2", "text": "Global services (logger, database) stay registered on the root"},
|
||||
{"id": "5.3", "text": "Per-request providers are registered on the request scope, not on the root"},
|
||||
{"id": "5.4", "text": "Mentions that the child scope inherits root providers"},
|
||||
{"id": "5.5", "text": "Does NOT register per-request providers globally where they would be shared across requests"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "container-not-passed-around",
|
||||
"description": "Tests that the container stays at the composition root",
|
||||
"prompt": "I'm using uber-go/dig in my Go service. My UserHandler depends on UserService and AuditLogger. Should I inject *dig.Container into UserHandler so it can resolve its own dependencies on demand?",
|
||||
"trap": "Without the skill, the model agrees to pass the container, turning it into a service-locator anti-pattern that hides dependencies and breaks testability.",
|
||||
"assertions": [
|
||||
{"id": "6.1", "text": "Advises against passing *dig.Container into business code"},
|
||||
{"id": "6.2", "text": "States that the container should only live at the composition root (main / startup)"},
|
||||
{"id": "6.3", "text": "Recommends UserHandler take typed dependencies (UserService, AuditLogger) as constructor parameters"},
|
||||
{"id": "6.4", "text": "Mentions service locator anti-pattern OR explains the testability/visibility downside"},
|
||||
{"id": "6.5", "text": "Does NOT show example code that injects the container into a handler"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "graph-validation-dryrun",
|
||||
"description": "Tests dig.DryRun for validating the graph in CI without running constructors",
|
||||
"prompt": "I want a Go test that catches missing-provider errors and cycles in my uber-go/dig wiring without actually starting database connections, HTTP servers, or any real side effects. How do I write that test?",
|
||||
"trap": "Without the skill, the model spins up a real container with real constructors, or skips validation entirely — missing dig.DryRun(true) which validates types without invocation.",
|
||||
"assertions": [
|
||||
{"id": "7.1", "text": "Uses dig.New(dig.DryRun(true)) to build the test container"},
|
||||
{"id": "7.2", "text": "Registers the same Provides as the production binary"},
|
||||
{"id": "7.3", "text": "Calls c.Invoke against the composition root (or top-level dependency) to trigger validation"},
|
||||
{"id": "7.4", "text": "Asserts no error is returned"},
|
||||
{"id": "7.5", "text": "Does NOT actually instantiate real services in the test"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "decorate-not-rewrite",
|
||||
"description": "Tests Decorate to wrap an existing value (e.g., logger) instead of replacing the constructor",
|
||||
"prompt": "In my Go app using uber-go/dig, I want every component in the 'worker' module to receive a *zap.Logger that is named 'worker' (i.e., adds a 'logger':'worker' field). Other modules should keep the unnamed logger. What's the cleanest way?",
|
||||
"trap": "Without the skill, the model rewrites NewLogger or wraps every constructor manually — missing c.Decorate which transforms the value at the scope/module boundary.",
|
||||
"assertions": [
|
||||
{"id": "8.1", "text": "Uses Decorate (c.Decorate or scope.Decorate) on *zap.Logger"},
|
||||
{"id": "8.2", "text": "The decorator returns log.Named(\"worker\") (or equivalent)"},
|
||||
{"id": "8.3", "text": "Decorate is applied at the worker scope/module, not globally"},
|
||||
{"id": "8.4", "text": "Does NOT modify or duplicate the original NewLogger constructor"},
|
||||
{"id": "8.5", "text": "Mentions decorator scope semantics (applies to scope and descendants only) OR places Decorate inside a scope"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "panic-recovery-option",
|
||||
"description": "Tests RecoverFromPanics container option",
|
||||
"prompt": "Some third-party constructors I'm registering with uber-go/dig occasionally panic on invalid configuration. I want my Go app to convert those panics into error returns from c.Invoke instead of crashing the process. How do I configure the container?",
|
||||
"trap": "Without the skill, the model wraps every constructor in defer/recover — missing dig.RecoverFromPanics() which does this at the container level.",
|
||||
"assertions": [
|
||||
{"id": "9.1", "text": "Uses dig.New(dig.RecoverFromPanics()) (or passes the option to dig.New)"},
|
||||
{"id": "9.2", "text": "Mentions or shows that the panic surfaces as a typed dig.PanicError"},
|
||||
{"id": "9.3", "text": "Does NOT recommend wrapping every constructor in defer recover() manually"},
|
||||
{"id": "9.4", "text": "Uses errors.As(err, &dig.PanicError{}) or equivalent to detect the panic case"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "groups-flatten-tag",
|
||||
"description": "Tests group:\",flatten\" tag when one constructor produces multiple group entries",
|
||||
"prompt": "I have a NewMigrations constructor in my Go app (using uber-go/dig) that returns a slice of Migration values. I want every Migration in this slice to be visible to consumers that consume the 'migrations' group as []Migration. How do I do this without nesting?",
|
||||
"trap": "Without the skill, the model registers []Migration as a single group entry, leaving consumers with [][]Migration. The right answer is the ',flatten' tag on the group.",
|
||||
"assertions": [
|
||||
{"id": "10.1", "text": "Uses group:\"migrations,flatten\" tag (with the flatten suffix)"},
|
||||
{"id": "10.2", "text": "Result struct has a slice field []Migration with the tag"},
|
||||
{"id": "10.3", "text": "Consumer receives []Migration (flat slice), not [][]Migration"},
|
||||
{"id": "10.4", "text": "Does NOT silently produce a nested-slice consumer signature"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "fx-vs-dig-when-lifecycle",
|
||||
"description": "Tests recommending fx (instead of raw dig) when the user needs lifecycle/signal handling",
|
||||
"prompt": "I'm starting a new Go service with uber-go/dig. The service needs to gracefully shut down on SIGTERM, run startup migrations, and start a background worker. Should I implement signal handling and start/stop sequencing on top of dig myself?",
|
||||
"trap": "Without the skill, the model writes custom signal handling code on top of raw dig — missing that uber-go/fx is built specifically for this and provides fx.Lifecycle, fx.Hook, and signal-aware Run().",
|
||||
"assertions": [
|
||||
{"id": "11.1", "text": "Recommends migrating to or considering uber-go/fx for the lifecycle requirements"},
|
||||
{"id": "11.2", "text": "Mentions that fx is built on top of dig (so existing wiring patterns transfer)"},
|
||||
{"id": "11.3", "text": "Mentions fx.Lifecycle / fx.Hook (OnStart/OnStop) for graceful boot/shutdown"},
|
||||
{"id": "11.4", "text": "Mentions app.Run() handling SIGINT/SIGTERM"},
|
||||
{"id": "11.5", "text": "Does NOT walk the user through writing custom signal handling on top of raw dig as the primary recommendation"}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,119 @@
|
||||
# Advanced — uber-go/dig
|
||||
|
||||
Detail topics that are referenced from `SKILL.md`. Each section is self-contained.
|
||||
|
||||
## Decorate
|
||||
|
||||
`Decorate` modifies a value already provided in the container — the decorator receives the original instance and returns a replacement. Common uses: enriching a logger with context, wrapping a metrics scope with tags, swapping a real client for a recording one in a child scope.
|
||||
|
||||
```go
|
||||
c.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("worker")
|
||||
})
|
||||
```
|
||||
|
||||
Decorators apply to the scope they were registered in and to that scope's descendants. Use them at scope boundaries or package wiring boundaries, not in main(), so changes stay local.
|
||||
|
||||
## Scopes
|
||||
|
||||
A `Scope` is a child container that inherits providers from its parent and can add or override its own. Scopes let request-, tenant-, or module-level dependencies coexist with shared singletons:
|
||||
|
||||
```go
|
||||
root := dig.New()
|
||||
root.Provide(NewLogger)
|
||||
root.Provide(NewDatabase)
|
||||
|
||||
requestScope := root.Scope("request")
|
||||
requestScope.Provide(NewRequestContext) // only visible inside requestScope
|
||||
requestScope.Decorate(func(l *zap.Logger) *zap.Logger {
|
||||
return l.With(zap.String("scope", "request"))
|
||||
})
|
||||
```
|
||||
|
||||
By default, providers registered to a scope are private to that scope and its children. Pass `dig.Export(true)` to `Provide` inside a scope to make the type visible from the parent:
|
||||
|
||||
```go
|
||||
requestScope.Provide(NewSharedCache, dig.Export(true))
|
||||
```
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
`optional:"true"` lets a consumer compile and run when a provider is missing. Use it sparingly — optional dependencies hide configuration mistakes. They make sense for genuinely optional features (a tracing exporter, an in-memory cache) but not for core services like a database.
|
||||
|
||||
```go
|
||||
type Params struct {
|
||||
dig.In
|
||||
|
||||
Logger *zap.Logger
|
||||
Tracer trace.Tracer `optional:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
dig wraps the constructor error with the dependency path so you can see *which* graph edge failed:
|
||||
|
||||
```go
|
||||
if err := c.Invoke(run); err != nil {
|
||||
// err describes the chain: "could not build *http.Server: ..."
|
||||
}
|
||||
```
|
||||
|
||||
Useful helpers:
|
||||
|
||||
- `errors.As(err, &dig.Error{})` — true if the error originated inside dig
|
||||
- `dig.RootCause(err)` — unwrap to the original constructor error returned by user code
|
||||
- `dig.IsCycleDetected(err)` — true if the graph contains a cycle (typically reported at first `Invoke` unless `DeferAcyclicVerification` is set)
|
||||
- `errors.As(err, &dig.PanicError{})` — when `RecoverFromPanics` is enabled, a panicking constructor surfaces as this typed error
|
||||
|
||||
## Visualization
|
||||
|
||||
dig can emit the dependency graph in DOT format — useful when wiring becomes too tangled to reason about by reading code:
|
||||
|
||||
```go
|
||||
f, _ := os.Create("graph.dot")
|
||||
_ = dig.Visualize(c, f)
|
||||
// then: dot -Tpng graph.dot -o graph.png
|
||||
```
|
||||
|
||||
`dig.VisualizeError(err)` highlights the failed edges when an `Invoke` returns an error — invaluable for debugging "missing type" failures in deep graphs.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Container
|
||||
|
||||
| Function/Method | Purpose |
|
||||
| -------------------------------- | -------------------------------------------------------- |
|
||||
| `dig.New(opts...)` | Create a root container |
|
||||
| `c.Provide(ctor, opts...)` | Register a constructor |
|
||||
| `c.Invoke(fn, opts...)` | Run a function with injected dependencies |
|
||||
| `c.Decorate(fn, opts...)` | Modify a previously-provided value within a scope |
|
||||
| `c.Scope(name, opts...)` | Create a child scope (private providers by default) |
|
||||
| `c.String()` | Human-readable text summary of providers (not DOT; use `dig.Visualize` for DOT) |
|
||||
|
||||
### Provide options
|
||||
|
||||
| Option | Purpose |
|
||||
| --------------------------------- | -------------------------------------------------------- |
|
||||
| `dig.Name("...")` | Disambiguate same-typed providers |
|
||||
| `dig.Group("...")` | Add the result to a value group |
|
||||
| `dig.As(new(I))` | Provide the concrete value as one or more interfaces |
|
||||
| `dig.Export(true)` | Make a scope-level provider visible from the root |
|
||||
| `dig.FillProvideInfo(&info)` | Capture metadata for tooling |
|
||||
|
||||
### Container options
|
||||
|
||||
| Option | Purpose |
|
||||
| ----------------------------------- | -------------------------------------------------------- |
|
||||
| `dig.DeferAcyclicVerification()` | Defer cycle check to first `Invoke` |
|
||||
| `dig.RecoverFromPanics()` | Convert constructor panics into `dig.PanicError` |
|
||||
| `dig.DryRun(true)` | Validate without invoking constructors |
|
||||
|
||||
### Errors
|
||||
|
||||
| Helper | Purpose |
|
||||
| ----------------------------------- | -------------------------------------------------------- |
|
||||
| `dig.RootCause(err)` | Unwrap to the user-returned error |
|
||||
| `dig.IsCycleDetected(err)` | True if the graph has a cycle |
|
||||
| `errors.As(err, &dig.PanicError{})` | Detect a recovered panic |
|
||||
| `dig.Visualize(c, w, opts...)` | Write the graph in DOT format |
|
||||
@@ -0,0 +1,264 @@
|
||||
# Recipes — uber-go/dig
|
||||
|
||||
End-to-end examples that go beyond the SKILL.md basics. Each recipe is self-contained and shows a real wiring problem.
|
||||
|
||||
## HTTP server with route group
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// Each handler contributes one route to the "routes" group.
|
||||
type RouteResult struct {
|
||||
dig.Out
|
||||
Route Route `group:"routes"`
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Pattern string
|
||||
Handler http.Handler
|
||||
}
|
||||
|
||||
func NewHealthRoute() RouteResult {
|
||||
return RouteResult{Route: Route{
|
||||
Pattern: "/health",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
}}
|
||||
}
|
||||
|
||||
func NewUserRoute(repo *UserRepo) RouteResult {
|
||||
return RouteResult{Route: Route{
|
||||
Pattern: "/users",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := repo.List(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "%d users", len(users))
|
||||
}),
|
||||
}}
|
||||
}
|
||||
|
||||
// The server consumes every Route registered to "routes".
|
||||
type ServerParams struct {
|
||||
dig.In
|
||||
Routes []Route `group:"routes"`
|
||||
}
|
||||
|
||||
func NewServer(p ServerParams) *http.Server {
|
||||
mux := http.NewServeMux()
|
||||
for _, r := range p.Routes {
|
||||
mux.Handle(r.Pattern, r.Handler)
|
||||
}
|
||||
return &http.Server{Addr: ":8080", Handler: mux}
|
||||
}
|
||||
|
||||
func main() {
|
||||
c := dig.New()
|
||||
|
||||
must(c.Provide(NewDB)) // *sql.DB
|
||||
must(c.Provide(NewUserRepo)) // *UserRepo
|
||||
must(c.Provide(NewHealthRoute)) // adds to group
|
||||
must(c.Provide(NewUserRoute)) // adds to group
|
||||
must(c.Provide(NewServer))
|
||||
|
||||
err := c.Invoke(func(srv *http.Server) error {
|
||||
log.Println("listening on", srv.Addr)
|
||||
return srv.ListenAndServe()
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Two databases (read-write + read-only)
|
||||
|
||||
```go
|
||||
type DBResult struct {
|
||||
dig.Out
|
||||
Primary *sql.DB `name:"primary"`
|
||||
ReadOnly *sql.DB `name:"readonly"`
|
||||
}
|
||||
|
||||
func NewDatabases(cfg *Config) (DBResult, error) {
|
||||
rw, err := sql.Open("postgres", cfg.PrimaryDSN)
|
||||
if err != nil {
|
||||
return DBResult{}, fmt.Errorf("primary: %w", err)
|
||||
}
|
||||
ro, err := sql.Open("postgres", cfg.ReadOnlyDSN)
|
||||
if err != nil {
|
||||
rw.Close()
|
||||
return DBResult{}, fmt.Errorf("readonly: %w", err)
|
||||
}
|
||||
return DBResult{Primary: rw, ReadOnly: ro}, nil
|
||||
}
|
||||
|
||||
type RepoParams struct {
|
||||
dig.In
|
||||
Writer *sql.DB `name:"primary"`
|
||||
Reader *sql.DB `name:"readonly"`
|
||||
}
|
||||
|
||||
func NewUserRepo(p RepoParams) *UserRepo {
|
||||
return &UserRepo{w: p.Writer, r: p.Reader}
|
||||
}
|
||||
```
|
||||
|
||||
## Provide as interface (`dig.As`) to hide concrete types
|
||||
|
||||
```go
|
||||
type Cache interface {
|
||||
Get(key string) (string, bool)
|
||||
Set(key, value string)
|
||||
}
|
||||
|
||||
type RedisCache struct {
|
||||
client *redis.Client
|
||||
metrics *Metrics // an internal field consumers should not see
|
||||
}
|
||||
|
||||
func NewRedisCache(client *redis.Client, m *Metrics) *RedisCache {
|
||||
return &RedisCache{client: client, metrics: m}
|
||||
}
|
||||
|
||||
func (c *RedisCache) Get(key string) (string, bool) { /* ... */ }
|
||||
func (c *RedisCache) Set(key, value string) { /* ... */ }
|
||||
|
||||
func main() {
|
||||
c := dig.New()
|
||||
must(c.Provide(NewRedisClient))
|
||||
must(c.Provide(NewMetrics))
|
||||
// Consumers see Cache, never *RedisCache or its internals.
|
||||
must(c.Provide(NewRedisCache, dig.As(new(Cache))))
|
||||
must(c.Invoke(func(cache Cache) {
|
||||
cache.Set("hello", "world")
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Request-scoped dependencies
|
||||
|
||||
A child scope inherits its parent's providers but adds request-local ones:
|
||||
|
||||
```go
|
||||
root := dig.New()
|
||||
must(root.Provide(NewLogger))
|
||||
must(root.Provide(NewDB))
|
||||
must(root.Provide(NewHandler)) // *Handler is shared; the scope inherits it
|
||||
|
||||
func handle(w http.ResponseWriter, req *http.Request) {
|
||||
scope := root.Scope("request")
|
||||
|
||||
// Request-scoped values
|
||||
must(scope.Provide(func() *http.Request { return req }))
|
||||
must(scope.Provide(func() RequestID { return RequestID(req.Header.Get("X-Request-ID")) }))
|
||||
must(scope.Decorate(func(l *zap.Logger) *zap.Logger {
|
||||
return l.With(zap.String("request_id", req.Header.Get("X-Request-ID")))
|
||||
}))
|
||||
|
||||
err := scope.Invoke(func(h *Handler) error {
|
||||
return h.Serve(w, req)
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The decorator only applies inside the request scope — sibling scopes (other in-flight requests) keep their own logger.
|
||||
|
||||
## Optional dependency for graceful degradation
|
||||
|
||||
```go
|
||||
type WorkerParams struct {
|
||||
dig.In
|
||||
|
||||
DB *sql.DB
|
||||
Tracer trace.Tracer `optional:"true"` // app still boots without OTel
|
||||
}
|
||||
|
||||
func NewWorker(p WorkerParams) *Worker {
|
||||
w := &Worker{db: p.DB}
|
||||
if p.Tracer != nil {
|
||||
w.tracer = p.Tracer
|
||||
} else {
|
||||
w.tracer = trace.NewNoopTracerProvider().Tracer("noop")
|
||||
}
|
||||
return w
|
||||
}
|
||||
```
|
||||
|
||||
Reach for `optional` only when the dependency is genuinely optional — a missing DB hidden behind `optional` becomes a nil-pointer panic at first use.
|
||||
|
||||
## Decorate to add cross-cutting behavior
|
||||
|
||||
```go
|
||||
// Wrap the *sql.DB with a metrics-recording wrapper everywhere.
|
||||
must(c.Decorate(func(db *sql.DB, m *Metrics) *sql.DB {
|
||||
return wrapWithMetrics(db, m)
|
||||
}))
|
||||
|
||||
// Wrap the logger with service tags.
|
||||
must(c.Decorate(func(log *zap.Logger, cfg *Config) *zap.Logger {
|
||||
return log.With(
|
||||
zap.String("service", cfg.ServiceName),
|
||||
zap.String("env", cfg.Env),
|
||||
)
|
||||
}))
|
||||
```
|
||||
|
||||
Decorators are scope-local. A decorator on the root applies everywhere; a decorator on a child scope only applies to that subtree.
|
||||
|
||||
## DryRun for graph validation in tests
|
||||
|
||||
```go
|
||||
func TestWiringIsValid(t *testing.T) {
|
||||
c := dig.New(dig.DryRun(true))
|
||||
|
||||
// Register everything main() registers
|
||||
must := func(err error) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
must(c.Provide(NewConfig))
|
||||
must(c.Provide(NewLogger))
|
||||
must(c.Provide(NewDB))
|
||||
must(c.Provide(NewServer))
|
||||
|
||||
// Invoke the composition root: dig validates types without running constructors.
|
||||
require.NoError(t, c.Invoke(func(*http.Server) {}))
|
||||
}
|
||||
```
|
||||
|
||||
This catches "no provider for *X" failures at build time instead of in production.
|
||||
|
||||
## Visualizing a failed graph
|
||||
|
||||
```go
|
||||
err := c.Invoke(run)
|
||||
if err != nil {
|
||||
f, _ := os.Create("graph.dot")
|
||||
defer f.Close()
|
||||
_ = dig.Visualize(c, f, dig.VisualizeError(err))
|
||||
log.Fatalf("wiring failed (graph in graph.dot): %v", err)
|
||||
}
|
||||
// Render: dot -Tpng graph.dot -o graph.png
|
||||
```
|
||||
|
||||
`VisualizeError` highlights the missing edges in red — much faster than reading the wrapped error chain.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Testing with uber-go/dig
|
||||
|
||||
dig containers are cheap to create. Build a fresh one per test, override what you need, drive the system with `Invoke`.
|
||||
|
||||
## Per-test container
|
||||
|
||||
```go
|
||||
func TestUserService_Create(t *testing.T) {
|
||||
c := dig.New()
|
||||
|
||||
fakeDB := &fakeDatabase{}
|
||||
require.NoError(t, c.Provide(func() Database { return fakeDB }))
|
||||
require.NoError(t, c.Provide(NewUserService))
|
||||
|
||||
require.NoError(t, c.Invoke(func(s *UserService) {
|
||||
err := s.Create(context.Background(), "alice@example.com")
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
require.Len(t, fakeDB.inserted, 1)
|
||||
}
|
||||
```
|
||||
|
||||
## Shared test wiring
|
||||
|
||||
For larger suites, factor the common providers into a helper:
|
||||
|
||||
```go
|
||||
func newTestContainer(t *testing.T, overrides ...func(*dig.Container)) *dig.Container {
|
||||
t.Helper()
|
||||
c := dig.New()
|
||||
require.NoError(t, c.Provide(NewTestLogger))
|
||||
require.NoError(t, c.Provide(NewInMemoryCache))
|
||||
require.NoError(t, c.Provide(func() Database { return &fakeDatabase{} }))
|
||||
require.NoError(t, c.Provide(NewUserService))
|
||||
|
||||
for _, override := range overrides {
|
||||
override(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestUserService_NotFound(t *testing.T) {
|
||||
c := newTestContainer(t, func(c *dig.Container) {
|
||||
// Replace the default DB with one that returns sql.ErrNoRows.
|
||||
require.NoError(t, c.Decorate(func(db Database) Database {
|
||||
return ¬FoundDB{Database: db}
|
||||
}))
|
||||
})
|
||||
|
||||
require.NoError(t, c.Invoke(func(s *UserService) {
|
||||
_, err := s.Get(context.Background(), "missing")
|
||||
require.ErrorIs(t, err, ErrUserNotFound)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
`Decorate` is the cleanest way to swap a dependency in tests — the test reads almost like production wiring with one extra line.
|
||||
|
||||
## Validate the production graph in CI
|
||||
|
||||
```go
|
||||
func TestProductionGraph(t *testing.T) {
|
||||
c := dig.New(dig.DryRun(true))
|
||||
|
||||
// Replicate every Provide() from main()
|
||||
require.NoError(t, registerAll(c))
|
||||
|
||||
// Invoke the same root the production binary does
|
||||
require.NoError(t, c.Invoke(func(*http.Server, *Worker, *MetricsExporter) {}))
|
||||
}
|
||||
```
|
||||
|
||||
`DryRun(true)` skips constructor execution — the graph is validated structurally. This catches missing-provider and type-mismatch errors without spinning up real DB connections.
|
||||
|
||||
## Detecting cycles before deploy
|
||||
|
||||
A cyclic graph fails at the first `Invoke` (or at `Provide` time when `DeferAcyclicVerification` is off, which is the default):
|
||||
|
||||
```go
|
||||
func TestNoCycles(t *testing.T) {
|
||||
c := dig.New()
|
||||
require.NoError(t, registerAll(c))
|
||||
|
||||
err := c.Invoke(func(*App) {})
|
||||
require.False(t, dig.IsCycleDetected(err), "cycle in dependency graph: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Asserting a constructor's error path
|
||||
|
||||
When a constructor returns an error, dig wraps it with the dependency path. Use `dig.RootCause` to assert the original error:
|
||||
|
||||
```go
|
||||
func TestDBProvider_BadDSN(t *testing.T) {
|
||||
c := dig.New()
|
||||
require.NoError(t, c.Provide(func() *Config {
|
||||
return &Config{DSN: "not a dsn"}
|
||||
}))
|
||||
require.NoError(t, c.Provide(NewDB))
|
||||
|
||||
err := c.Invoke(func(*sql.DB) {})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, dig.RootCause(err), "invalid connection string")
|
||||
}
|
||||
```
|
||||
|
||||
## Recovering from constructor panics
|
||||
|
||||
Wrap constructors that may panic on misuse so the test reports a typed error instead of crashing the runner:
|
||||
|
||||
```go
|
||||
c := dig.New(dig.RecoverFromPanics())
|
||||
|
||||
require.NoError(t, c.Provide(func() *App {
|
||||
panic("intentionally broken")
|
||||
}))
|
||||
|
||||
err := c.Invoke(func(*App) {})
|
||||
|
||||
var pe dig.PanicError
|
||||
require.True(t, errors.As(err, &pe))
|
||||
require.Contains(t, pe.Error(), "intentionally broken")
|
||||
```
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
name: golang-uber-fx
|
||||
description: "Golang application framework using uber-go/fx — fx.New, fx.Provide, fx.Invoke, fx.Module, fx.Lifecycle hooks, fx.Annotate (name/group/As), fx.Decorate, fx.Supply, fx.Replace, fx.WithLogger, and signal-aware Run(). Apply when using or adopting uber-go/fx, when the codebase imports `go.uber.org/fx`, or when wiring services with fx.New. For raw DI without lifecycle, see `samber/cc-skills-golang@golang-uber-dig` skill."
|
||||
user-invocable: true
|
||||
license: MIT
|
||||
compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.
|
||||
metadata:
|
||||
author: samber
|
||||
version: "1.0.0"
|
||||
openclaw:
|
||||
emoji: "🏭"
|
||||
homepage: https://github.com/samber/cc-skills-golang
|
||||
requires:
|
||||
bins:
|
||||
- go
|
||||
install: []
|
||||
skill-library-version: "1.24.0"
|
||||
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs
|
||||
---
|
||||
|
||||
**Persona:** You are a Go architect building a long-running service with fx. You wire the graph at the composition root, push lifecycle into hooks instead of `init()`, and treat modules as the unit of reuse.
|
||||
|
||||
# Using uber-go/fx for Application Wiring in Go
|
||||
|
||||
Application framework combining a reflection-based DI container (built on `uber-go/dig`) with a lifecycle, module system, signal-aware run loop, and structured event logging. For long-running services where boot order, graceful shutdown, and modular composition matter.
|
||||
|
||||
**Official Resources:**
|
||||
|
||||
- [pkg.go.dev/go.uber.org/fx](https://pkg.go.dev/go.uber.org/fx)
|
||||
- [uber-go.github.io/fx](https://uber-go.github.io/fx/)
|
||||
- [github.com/uber-go/fx](https://github.com/uber-go/fx)
|
||||
|
||||
This skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform.
|
||||
|
||||
```bash
|
||||
go get go.uber.org/fx
|
||||
```
|
||||
|
||||
> **fx vs. dig.** fx wraps dig and adds lifecycle hooks (`fx.Lifecycle`), modules (`fx.Module`), `Run()` with signal handling, structured event logs (`fxevent`), and ergonomic helpers. Use raw dig (`samber/cc-skills-golang@golang-uber-dig` skill) only when you don't need lifecycle or app boot — most production services should use fx.
|
||||
|
||||
## The Application
|
||||
|
||||
```go
|
||||
import "go.uber.org/fx"
|
||||
|
||||
app := fx.New(
|
||||
fx.Provide(NewLogger, NewDatabase, NewServer),
|
||||
fx.Invoke(RegisterRoutes),
|
||||
)
|
||||
app.Run() // blocks until SIGINT/SIGTERM, then runs OnStop hooks
|
||||
```
|
||||
|
||||
Boot stages: `fx.New` validates types (constructors do not run); `app.Start(ctx)` runs each `fx.Invoke` and fires OnStart hooks in topological order; main blocks on `app.Done()`; `app.Stop(ctx)` fires OnStop hooks in reverse order. Default timeout is **15 seconds** — override with `fx.StartTimeout` / `fx.StopTimeout`.
|
||||
|
||||
## Provide and Invoke
|
||||
|
||||
```go
|
||||
fx.New(
|
||||
fx.Provide(NewLogger, NewDatabase, NewServer), // lazy
|
||||
fx.Invoke(RegisterRoutes, StartMetricsExporter), // always run during Start
|
||||
)
|
||||
```
|
||||
|
||||
`fx.Provide` registers constructors; `fx.Invoke` is the trigger — without an Invoke (directly or transitively) referencing a type, its constructor never runs.
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
Inject `fx.Lifecycle` and append hooks. Constructors should return quickly; long-running work belongs in `OnStart`.
|
||||
|
||||
```go
|
||||
func NewHTTPServer(lc fx.Lifecycle, log *zap.Logger, cfg *Config) *http.Server {
|
||||
srv := &http.Server{Addr: cfg.Addr}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
ln, err := net.Listen("tcp", srv.Addr)
|
||||
if err != nil { return err }
|
||||
go srv.Serve(ln) // blocking work in a goroutine
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return srv.Shutdown(ctx)
|
||||
},
|
||||
})
|
||||
return srv
|
||||
}
|
||||
```
|
||||
|
||||
Both callbacks receive a context bounded by `StartTimeout`/`StopTimeout` — respect cancellation. **OnStart must return quickly** — spawn a goroutine for blocking work; otherwise startup hangs and dependent hooks never fire.
|
||||
|
||||
`fx.StartHook` / `fx.StopHook` / `fx.StartStopHook` adapt simpler signatures (no context, no error, or both):
|
||||
|
||||
```go
|
||||
lc.Append(fx.StartStopHook(srv.Start, srv.Stop)) // matched pair
|
||||
```
|
||||
|
||||
## Parameter and Result Objects
|
||||
|
||||
fx re-exports dig's `dig.In` / `dig.Out` as `fx.In` / `fx.Out`. Use them when a constructor has 4+ dependencies, or when you need `name`/`group`/`optional` tags.
|
||||
|
||||
```go
|
||||
type ServerParams struct {
|
||||
fx.In
|
||||
|
||||
Logger *zap.Logger
|
||||
DB *sql.DB
|
||||
Cache *redis.Client `optional:"true"`
|
||||
Routes []http.Handler `group:"routes"`
|
||||
}
|
||||
|
||||
func NewServer(p ServerParams) *Server { /* ... */ }
|
||||
```
|
||||
|
||||
## fx.Annotate
|
||||
|
||||
`fx.Annotate` wraps a constructor to add tags or interface bindings without a `fx.Out` struct. Prefer it for ergonomic name/group/As bindings:
|
||||
|
||||
```go
|
||||
fx.Provide(
|
||||
fx.Annotate(NewPrimaryDB, fx.ResultTags(`name:"primary"`)),
|
||||
fx.Annotate(NewPostgresDB, fx.As(new(Database))), // expose interface
|
||||
fx.Annotate(NewUserHandler,
|
||||
fx.As(new(http.Handler)),
|
||||
fx.ResultTags(`group:"routes"`),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Value Groups
|
||||
|
||||
Many constructors, one consumer slice — typical for routes, health checks, metrics collectors:
|
||||
|
||||
```go
|
||||
type RouteResult struct {
|
||||
fx.Out
|
||||
Handler http.Handler `group:"routes"`
|
||||
}
|
||||
|
||||
type ServerParams struct {
|
||||
fx.In
|
||||
Routes []http.Handler `group:"routes"`
|
||||
}
|
||||
```
|
||||
|
||||
Append `,flatten` (`group:"routes,flatten"`) to unwrap a slice instead of nesting it. Order is **not guaranteed** — provide an explicit ordered slice when sequence matters.
|
||||
|
||||
## fx.Module
|
||||
|
||||
`fx.Module` groups providers, invokes, and decorators under a name. Modules **scope decorators** to themselves and their children — a logger renamed in `fx.Module("db", ...)` only appears renamed for code inside that module.
|
||||
|
||||
```go
|
||||
var DatabaseModule = fx.Module("database",
|
||||
fx.Provide(NewConnection, NewUserRepository),
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("db")
|
||||
}),
|
||||
)
|
||||
|
||||
func main() {
|
||||
fx.New(
|
||||
fx.Provide(NewConfig, NewLogger),
|
||||
DatabaseModule,
|
||||
HTTPModule,
|
||||
).Run()
|
||||
}
|
||||
```
|
||||
|
||||
Treat each module as a small library that can be lifted into another app — its public surface is the types it Provides.
|
||||
|
||||
For `fx.Supply`/`fx.Replace`/`fx.Decorate`, optional deps, custom logging, manual lifecycle, and Quick Reference, see [advanced.md](./references/advanced.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Keep `main()` thin — providers, modules, and a single `Run()`. Push real work into modules so each can be tested in isolation.
|
||||
2. Use lifecycle hooks instead of `init()` or goroutines launched from constructors — Start/Stop ordering depends on graph topology, but `init()` goroutines do not, which leads to races and leaks.
|
||||
3. OnStart must return promptly — long work goes in a goroutine inside the hook. A blocking OnStart hangs the rest of the boot.
|
||||
4. Respect `ctx.Done()` in hooks — a hook that ignores cancellation is reported as a timeout failure but its goroutine continues, leaking resources.
|
||||
5. Group by module, not by layer — a module owns the providers, lifecycle, and decorators for one concern (HTTP, DB, metrics).
|
||||
6. Use `fx.Annotate` for tags rather than wrapping a constructor in an `fx.Out` struct — keeps the constructor reusable outside fx.
|
||||
7. Replace `fx.Provide` with `fx.Supply` for pre-built values (config, command-line flags). Shorter, signals intent.
|
||||
8. Validate the graph in CI by booting under `fx.New(...).Err()` — catches missing providers and cycles before deploy.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| Long-running work directly in OnStart | Spawn a goroutine inside OnStart; the hook itself must return quickly so dependent hooks can run. |
|
||||
| `fx.Provide` something that should be `fx.Supply` | Pre-built values (config, secrets) belong in `fx.Supply` — clearer and avoids a no-op constructor. |
|
||||
| Module decorator leaking to siblings | Decorate inside `fx.Module(...)` — decorators flow only to descendants. A top-level `fx.Decorate` is global. |
|
||||
| Group order assumed | Groups are unordered. If order matters, provide an ordered slice from one constructor. |
|
||||
| Constructors with side effects | Side effects belong in OnStart — constructors should be cheap and pure-ish, since they may run concurrently and lazily. |
|
||||
| Forgotten `fx.Invoke` | Without an Invoke (or downstream consumer), constructors never run. Add at least one Invoke per app. |
|
||||
|
||||
## Testing
|
||||
|
||||
Use `go.uber.org/fx/fxtest` to integrate fx with `*testing.T` (failures call `t.Fatal`, `RequireStop` registers as `t.Cleanup`). `fx.Populate(&target)` pulls values out of the graph; `fx.Replace` swaps real dependencies for fakes. Full patterns in [testing.md](./references/testing.md).
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [advanced.md](./references/advanced.md) — Supply/Replace/Decorate, optional deps, custom event logging, manual lifecycle, full Quick Reference
|
||||
- [recipes.md](./references/recipes.md) — full HTTP service with database/metrics, background workers with graceful drain, multiple impls of the same interface, manual lifecycle for CLI embedding
|
||||
- [testing.md](./references/testing.md) — fxtest patterns, `fx.Replace`, `fx.Populate`, isolated lifecycle tests, CI graph validation
|
||||
|
||||
## Cross-References
|
||||
|
||||
- → See `samber/cc-skills-golang@golang-uber-dig` skill for the underlying container, `dig.In`/`dig.Out`, and DI without lifecycle
|
||||
- → See `samber/cc-skills-golang@golang-dependency-injection` skill for DI concepts and library comparison
|
||||
- → See `samber/cc-skills-golang@golang-samber-do` skill for a generics-based alternative without reflection
|
||||
- → See `samber/cc-skills-golang@golang-google-wire` skill for compile-time DI (no runtime container)
|
||||
- → See `samber/cc-skills-golang@golang-structs-interfaces` skill for interface design patterns
|
||||
- → See `samber/cc-skills-golang@golang-context` skill for context propagation in OnStart/OnStop hooks
|
||||
- → See `samber/cc-skills-golang@golang-testing` skill for general testing patterns
|
||||
|
||||
If you encounter a bug or unexpected behavior in uber-go/fx, open an issue at https://github.com/uber-go/fx/issues.
|
||||
@@ -0,0 +1,156 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "lifecycle-not-init",
|
||||
"description": "Tests use of fx.Lifecycle hooks instead of init() or constructor side effects for startup work",
|
||||
"prompt": "In my Go service using uber-go/fx, I have a NewHTTPServer constructor that should listen on a port and serve requests. Where should I call srv.Serve(ln) — inside the constructor, in init(), or somewhere else?",
|
||||
"trap": "Without the skill, the model calls srv.Serve in init() or directly inside the constructor (which would block boot), or writes a goroutine inside the constructor (which fires before lifecycle ordering applies). The right answer is OnStart with a goroutine.",
|
||||
"assertions": [
|
||||
{"id": "1.1", "text": "Injects fx.Lifecycle into NewHTTPServer"},
|
||||
{"id": "1.2", "text": "Calls lc.Append with an fx.Hook (or fx.StartHook/StopHook) — OnStart starts the server, OnStop calls Shutdown"},
|
||||
{"id": "1.3", "text": "OnStart launches srv.Serve inside a goroutine so the hook returns quickly"},
|
||||
{"id": "1.4", "text": "Does NOT call srv.Serve directly inside the constructor"},
|
||||
{"id": "1.5", "text": "Does NOT use init() to start the server"},
|
||||
{"id": "1.6", "text": "OnStop calls srv.Shutdown(ctx) for graceful shutdown"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "annotate-vs-fxout-struct",
|
||||
"description": "Tests fx.Annotate as the modern way to add tags or interface bindings",
|
||||
"prompt": "I have NewPostgresDB returning *PostgresDB in my Go app using uber-go/fx. I want consumers to ask for a Database interface, and I want this DB tagged with name:\"primary\" so I can add a replica later. Show me how.",
|
||||
"trap": "Without the skill, the model writes a separate adapter constructor returning Database, or wraps the result in an fx.Out struct — missing fx.Annotate(NewPostgresDB, fx.As(new(Database)), fx.ResultTags(...)) which does both in one line.",
|
||||
"assertions": [
|
||||
{"id": "2.1", "text": "Uses fx.Annotate around NewPostgresDB"},
|
||||
{"id": "2.2", "text": "Uses fx.As(new(Database)) inside the annotation to bind the interface"},
|
||||
{"id": "2.3", "text": "Uses fx.ResultTags(`name:\"primary\"`) for the named tag"},
|
||||
{"id": "2.4", "text": "Does NOT introduce a separate adapter/wrapper constructor"},
|
||||
{"id": "2.5", "text": "Does NOT rewrite NewPostgresDB to return Database directly (since the original constructor stays untouched)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "module-organization",
|
||||
"description": "Tests fx.Module for organizing related providers/invokes/decorators",
|
||||
"prompt": "My Go application using uber-go/fx is growing — main.go now has dozens of fx.Provide calls for HTTP, database, metrics, and worker concerns mixed together. How should I reorganize this?",
|
||||
"trap": "Without the skill, the model splits providers across several Go packages but keeps a flat list in main(), missing fx.Module which groups providers, invokes, and decorators under a name and lets decorators be module-scoped.",
|
||||
"assertions": [
|
||||
{"id": "3.1", "text": "Recommends fx.Module to group related options"},
|
||||
{"id": "3.2", "text": "Shows at least 2 separate modules (e.g., HTTPModule, DatabaseModule)"},
|
||||
{"id": "3.3", "text": "main() composes the modules via fx.New(HTTPModule, DatabaseModule, ...)"},
|
||||
{"id": "3.4", "text": "Mentions OR demonstrates that fx.Decorate inside a module is scoped to that module"},
|
||||
{"id": "3.5", "text": "Each module includes its own fx.Provide (and possibly fx.Invoke / fx.Decorate) calls"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "supply-vs-provide",
|
||||
"description": "Tests fx.Supply for pre-built values (config, secrets) instead of fx.Provide with a no-op constructor",
|
||||
"prompt": "In my Go application using uber-go/fx, I parse a *Config from flags and load an API_KEY environment variable in main() before calling fx.New. How should I make these available to the rest of the graph?",
|
||||
"trap": "Without the skill, the model writes fx.Provide(func() *Config { return cfg }) — a redundant constructor that just returns the existing value. fx.Supply does this without the boilerplate.",
|
||||
"assertions": [
|
||||
{"id": "4.1", "text": "Uses fx.Supply(cfg) (or fx.Supply with both values)"},
|
||||
{"id": "4.2", "text": "Does NOT wrap the pre-built values in fx.Provide(func() *Config { return cfg })"},
|
||||
{"id": "4.3", "text": "Mentions or demonstrates that fx.Supply makes pre-built values first-class graph members"},
|
||||
{"id": "4.4", "text": "If both values are supplied as the same type or need a tag, optionally uses fx.Annotate within fx.Supply for tagging"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "fxtest-with-populate",
|
||||
"description": "Tests fxtest.New + fx.Populate for testing instead of raw fx.New + fx.Invoke",
|
||||
"prompt": "I have a *UserService wired in my Go app using uber-go/fx. I want a unit test that pulls *UserService out of the graph (with a fake Database injected) and asserts behavior. Show me the minimal test.",
|
||||
"trap": "Without the skill, the model uses fx.New + fx.Invoke(func(s *UserService) { ... }) — works but doesn't fail the test cleanly and has no Cleanup integration. fxtest.New + fx.Populate is idiomatic.",
|
||||
"assertions": [
|
||||
{"id": "5.1", "text": "Uses fxtest.New(t, ...) instead of fx.New"},
|
||||
{"id": "5.2", "text": "Uses fx.Populate(&svc) to extract the *UserService from the graph"},
|
||||
{"id": "5.3", "text": "Calls app.RequireStart() (or .Start) and app.RequireStop() (e.g., as t.Cleanup or defer)"},
|
||||
{"id": "5.4", "text": "Provides a fake Database (interface) — does NOT use the real DB"},
|
||||
{"id": "5.5", "text": "Does NOT use fx.Invoke as the primary mechanism for extracting the service"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "replace-for-fakes",
|
||||
"description": "Tests fx.Replace inside fxtest to swap a real dependency embedded in a module",
|
||||
"prompt": "My Go app uses uber-go/fx with a ProductionModule that bundles all real wiring. In one integration test, I want to replace the real Database with an erroring fake — without rewriting the module. How?",
|
||||
"trap": "Without the skill, the model rewrites the module (or copies it) to inject the fake — missing fx.Replace which overrides a previously-provided type without touching the module.",
|
||||
"assertions": [
|
||||
{"id": "6.1", "text": "Uses fx.Replace(...) inside fxtest.New (or fx.New) to override the Database"},
|
||||
{"id": "6.2", "text": "Composes ProductionModule alongside fx.Replace (the module is reused unchanged)"},
|
||||
{"id": "6.3", "text": "Uses fx.Annotate inside fx.Replace if needed for fx.As(new(Database)) binding"},
|
||||
{"id": "6.4", "text": "Does NOT modify or duplicate the production module to inject the fake"},
|
||||
{"id": "6.5", "text": "Mentions that fx.Replace is appropriate for tests (not production code)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "value-groups-handlers",
|
||||
"description": "Tests value groups when many handler constructors must contribute to one slice",
|
||||
"prompt": "In my Go HTTP server using uber-go/fx, I want every NewXxxHandler constructor to register itself with the router automatically — no manual list of handlers in main(). I have NewUserHandler, NewPostHandler, NewHealthHandler. The router consumes []http.Handler. Wire this.",
|
||||
"trap": "Without the skill, the model assembles a slice manually in main() or writes one constructor that builds all handlers — missing the group:\"...\" tag pattern that keeps producers and the consumer decoupled.",
|
||||
"assertions": [
|
||||
{"id": "7.1", "text": "Each handler is registered with fx.Annotate(... fx.ResultTags(`group:\"routes\"`)) (or via an fx.Out struct with a group tag)"},
|
||||
{"id": "7.2", "text": "The router (or server) consumes a fx.In with []http.Handler tagged group:\"routes\""},
|
||||
{"id": "7.3", "text": "Optionally uses fx.As(new(http.Handler)) inside the annotation if the constructor returns a concrete type"},
|
||||
{"id": "7.4", "text": "Does NOT manually maintain a slice of handlers in main()"},
|
||||
{"id": "7.5", "text": "Does not assert ordering of the resulting slice (or explicitly notes order is unspecified)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "logger-fxevent-zap",
|
||||
"description": "Tests fx.WithLogger + fxevent.ZapLogger to route fx events through the app's structured logger",
|
||||
"prompt": "My Go service using uber-go/fx logs everything through *zap.Logger. The default fx output goes to stderr in a different format and is noisy in production. How do I route fx's own events (provide/invoke/start/stop) through my zap logger?",
|
||||
"trap": "Without the skill, the model suggests overriding os.Stderr or grepping logs — missing fx.WithLogger which lets you provide an fxevent.Logger backed by zap.",
|
||||
"assertions": [
|
||||
{"id": "8.1", "text": "Uses fx.WithLogger(...) as an fx.New option"},
|
||||
{"id": "8.2", "text": "The provided fxevent.Logger is &fxevent.ZapLogger{Logger: log}"},
|
||||
{"id": "8.3", "text": "The logger inside fx.WithLogger receives the *zap.Logger as a parameter (so fx wires it from the graph)"},
|
||||
{"id": "8.4", "text": "Does NOT redirect stderr or modify global log output"},
|
||||
{"id": "8.5", "text": "May mention fx.NopLogger as an option to silence fx events"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "manual-lifecycle-cli",
|
||||
"description": "Tests app.Start / app.Done / app.Stop for embedding fx in a larger program",
|
||||
"prompt": "I'm building a Go CLI tool that has an interactive sub-command and a serve sub-command. I want the serve sub-command to spin up an fx graph, start it, wait for SIGINT, and shut down — but I don't want fx hijacking the entire process via app.Run() (because the CLI may resume to other work after). How do I drive fx manually?",
|
||||
"trap": "Without the skill, the model calls app.Run() and then can't return to the CLI — missing manual Start/Done/Stop, which is exactly what the user is asking for.",
|
||||
"assertions": [
|
||||
{"id": "9.1", "text": "Uses app.Start(ctx) explicitly with a context (often timeout-bounded)"},
|
||||
{"id": "9.2", "text": "Waits on app.Done() (or a select including parent context cancellation) instead of calling app.Run()"},
|
||||
{"id": "9.3", "text": "Uses app.Stop(ctx) explicitly with a context"},
|
||||
{"id": "9.4", "text": "Does NOT recommend app.Run() as the primary mechanism for this scenario"},
|
||||
{"id": "9.5", "text": "Mentions that app.Err() can validate wiring without starting"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "onstart-non-blocking",
|
||||
"description": "Tests that long-running OnStart work is launched in a goroutine, not run synchronously",
|
||||
"prompt": "In my Go app using uber-go/fx, the OnStart hook for a worker calls a method that runs forever (consuming jobs from a queue until the app stops). Show me the OnStart implementation.",
|
||||
"trap": "Without the skill, the model calls the long-running method synchronously inside OnStart — which hangs startup. The right pattern is to spawn a goroutine and return nil quickly.",
|
||||
"assertions": [
|
||||
{"id": "10.1", "text": "OnStart launches the long-running method inside a goroutine"},
|
||||
{"id": "10.2", "text": "OnStart itself returns nil (or an error) quickly without waiting for the worker to finish"},
|
||||
{"id": "10.3", "text": "OnStop signals the worker to stop (closing a channel, calling Cancel, etc.) and waits for it to drain"},
|
||||
{"id": "10.4", "text": "Does NOT call the long-running method synchronously inside OnStart"},
|
||||
{"id": "10.5", "text": "Mentions that a blocking OnStart would hang the boot / dependent hooks"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "fx-when-not-dig",
|
||||
"description": "Tests recommending raw dig (instead of fx) when the user does not need lifecycle / app boot",
|
||||
"prompt": "I'm writing a one-shot Go CLI command that builds a small object graph (parses input, creates a few services, calls one of them, exits). I'm reading about uber-go/fx but it seems heavy. Should I use fx for this?",
|
||||
"trap": "Without the skill, the model unconditionally recommends fx — missing that for one-shot programs without lifecycle, raw uber-go/dig is the lighter, simpler choice.",
|
||||
"assertions": [
|
||||
{"id": "11.1", "text": "Recommends raw uber-go/dig (or notes fx is overkill for this case)"},
|
||||
{"id": "11.2", "text": "Mentions that fx is built on dig — the wiring patterns are nearly identical"},
|
||||
{"id": "11.3", "text": "Mentions that fx adds value when the program needs lifecycle hooks, signal handling, or modular composition"},
|
||||
{"id": "11.4", "text": "Does NOT recommend introducing fx.Lifecycle/fx.Module to a one-shot program"},
|
||||
{"id": "11.5", "text": "May recommend manual constructor injection if the graph is very small"}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,135 @@
|
||||
# Advanced — uber-go/fx
|
||||
|
||||
Detail topics referenced from `SKILL.md`. Each section is self-contained.
|
||||
|
||||
## fx.Supply, fx.Replace, fx.Decorate
|
||||
|
||||
| Option | Purpose |
|
||||
| ----------------------- | ----------------------------------------------------------------------------- |
|
||||
| `fx.Supply(values...)` | Provide pre-built values directly. Use for config, secrets, parsed flags. |
|
||||
| `fx.Replace(values...)` | Replace an already-provided type. Most useful in tests: swap real for fake. |
|
||||
| `fx.Decorate(fn)` | Wrap or modify an existing value. Scoped to the surrounding module. |
|
||||
|
||||
```go
|
||||
fx.Supply(cfg, secret)
|
||||
|
||||
// Replace inside fxtest
|
||||
fx.Replace(fx.Annotate(&fakeDB{}, fx.As(new(Database))))
|
||||
|
||||
// Decorate, module-scoped
|
||||
fx.Module("worker",
|
||||
fx.Decorate(func(s metrics.Scope) metrics.Scope {
|
||||
return s.Tagged(map[string]string{"component": "worker"})
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
`optional:"true"` lets a consumer compile and run when no provider exists. Use it for genuinely optional features (a tracer, a cache) — not for core services like a database.
|
||||
|
||||
```go
|
||||
type Params struct {
|
||||
fx.In
|
||||
|
||||
Logger *zap.Logger
|
||||
Tracer trace.Tracer `optional:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
## Logging fx Events
|
||||
|
||||
fx emits structured events (provide, invoke, hook execution, errors) through `fxevent.Logger`. By default it writes to stderr — replace with a Zap logger or silence it in tests:
|
||||
|
||||
```go
|
||||
fx.New(
|
||||
fx.Provide(NewZapLogger),
|
||||
fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
|
||||
return &fxevent.ZapLogger{Logger: log}
|
||||
}),
|
||||
// Or silence: fx.NopLogger
|
||||
)
|
||||
```
|
||||
|
||||
## Manual Lifecycle Control
|
||||
|
||||
`app.Run()` is convenient but inflexible. For tests, custom signal handling, or embedding fx in a larger program, drive the lifecycle manually:
|
||||
|
||||
```go
|
||||
app := fx.New(/* ... */)
|
||||
|
||||
startCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := app.Start(startCtx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
<-app.Done() // waits for SIGINT/SIGTERM
|
||||
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := app.Stop(stopCtx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
`fx.StartTimeout` and `fx.StopTimeout` set defaults; pass an explicit context to override per-call.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Application
|
||||
|
||||
| Function | Purpose |
|
||||
| ---------------------------- | -------------------------------------------------------- |
|
||||
| `fx.New(opts...)` | Build the application graph |
|
||||
| `app.Run()` | Start, wait for signal, Stop — single call |
|
||||
| `app.Start(ctx)` | Run OnStart hooks in dependency order |
|
||||
| `app.Stop(ctx)` | Run OnStop hooks in reverse order |
|
||||
| `app.Done()` | Channel that closes on SIGINT/SIGTERM |
|
||||
| `app.Err()` | Wiring error from `fx.New` (validate without starting) |
|
||||
|
||||
### Wiring
|
||||
|
||||
| Option | Purpose |
|
||||
| ---------------------------- | -------------------------------------------------------- |
|
||||
| `fx.Provide(ctors...)` | Register constructors |
|
||||
| `fx.Invoke(fns...)` | Run functions during Start |
|
||||
| `fx.Supply(values...)` | Provide pre-built values |
|
||||
| `fx.Replace(values...)` | Replace previously-provided values (tests) |
|
||||
| `fx.Decorate(fn)` | Wrap an existing value (module-scoped) |
|
||||
| `fx.Module(name, opts...)` | Group providers/invokes/decorators |
|
||||
| `fx.Options(opts...)` | Bundle options into a single value |
|
||||
| `fx.Populate(targets...)` | Extract typed values from the graph (tests) |
|
||||
|
||||
### Annotations
|
||||
|
||||
| Function | Purpose |
|
||||
| ------------------------------------- | ------------------------------------------------ |
|
||||
| `fx.Annotate(fn, opts...)` | Tag/interface-wrap a constructor |
|
||||
| `fx.ParamTags("...")` | Tag parameters of an annotated constructor |
|
||||
| `fx.ResultTags("...")` | Tag results of an annotated constructor |
|
||||
| `fx.As(new(I))` | Provide as one or more interfaces |
|
||||
| `fx.From(types...)` | Bind annotated parameters to specific provided types |
|
||||
|
||||
### Lifecycle
|
||||
|
||||
| Helper | Purpose |
|
||||
| -------------------------------------------- | --------------------------------------------- |
|
||||
| `fx.Hook{OnStart, OnStop}` | Full hook with context-aware callbacks |
|
||||
| `fx.StartHook(fn)` | Adapt a simple Start function |
|
||||
| `fx.StopHook(fn)` | Adapt a simple Stop function |
|
||||
| `fx.StartStopHook(start, stop)` | Pair of simple Start/Stop functions |
|
||||
| `fx.StartTimeout(d)`, `fx.StopTimeout(d)` | Override default 15s lifecycle timeouts |
|
||||
| `fx.ErrorHook(h)` | Intercept lifecycle errors (e.g. failed OnStart) for alerting or cleanup |
|
||||
|
||||
### Logging & Testing
|
||||
|
||||
| Helper | Purpose |
|
||||
| -------------------------------------------- | --------------------------------------------- |
|
||||
| `fx.WithLogger(fn)` | Plug in a custom `fxevent.Logger` |
|
||||
| `fx.NopLogger` | Silence fx event logging |
|
||||
| `fxevent.ZapLogger{Logger: log}` | Bridge fx events into zap |
|
||||
| `fxevent.SlogLogger{Logger: log}` | Bridge fx events into log/slog |
|
||||
| `fxtest.New(t, opts...)` | App that fails the test on errors |
|
||||
| `app.RequireStart()`, `app.RequireStop()` | Start/Stop with `t.Fatal` on failure |
|
||||
| `fxtest.NewLifecycle(t)` | Standalone lifecycle for unit tests |
|
||||
@@ -0,0 +1,331 @@
|
||||
# Recipes — uber-go/fx
|
||||
|
||||
End-to-end examples that go beyond the SKILL.md basics. Each recipe is self-contained and shows a real wiring problem.
|
||||
|
||||
## Full HTTP service with database, metrics, and graceful shutdown
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fx.New(
|
||||
fx.Provide(
|
||||
NewConfig,
|
||||
NewLogger,
|
||||
NewDatabase,
|
||||
NewMetricsRegistry,
|
||||
),
|
||||
|
||||
DatabaseModule,
|
||||
HTTPModule,
|
||||
MetricsModule,
|
||||
|
||||
fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
|
||||
return &fxevent.ZapLogger{Logger: log}
|
||||
}),
|
||||
|
||||
fx.StartTimeout(30 * time.Second),
|
||||
fx.StopTimeout(30 * time.Second),
|
||||
).Run()
|
||||
}
|
||||
|
||||
var DatabaseModule = fx.Module("database",
|
||||
fx.Provide(
|
||||
NewUserRepository,
|
||||
NewPostRepository,
|
||||
),
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("db")
|
||||
}),
|
||||
)
|
||||
|
||||
var HTTPModule = fx.Module("http",
|
||||
fx.Provide(
|
||||
NewRouter,
|
||||
NewHTTPServer,
|
||||
// Each handler joins the "routes" group.
|
||||
AsRoute(NewUserHandler),
|
||||
AsRoute(NewPostHandler),
|
||||
AsRoute(NewHealthHandler),
|
||||
),
|
||||
fx.Invoke(func(*http.Server) {}), // forces server to be built
|
||||
)
|
||||
|
||||
var MetricsModule = fx.Module("metrics",
|
||||
fx.Provide(NewPrometheusHandler),
|
||||
fx.Invoke(RegisterMetrics),
|
||||
)
|
||||
|
||||
// Helper to register a handler with the "routes" group.
|
||||
func AsRoute(ctor any) any {
|
||||
return fx.Annotate(
|
||||
ctor,
|
||||
fx.As(new(Route)),
|
||||
fx.ResultTags(`group:"routes"`),
|
||||
)
|
||||
}
|
||||
|
||||
type Route interface {
|
||||
Pattern() string
|
||||
http.Handler
|
||||
}
|
||||
|
||||
type RouterParams struct {
|
||||
fx.In
|
||||
Routes []Route `group:"routes"`
|
||||
}
|
||||
|
||||
func NewRouter(p RouterParams) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
for _, r := range p.Routes {
|
||||
mux.Handle(r.Pattern(), r)
|
||||
}
|
||||
return mux
|
||||
}
|
||||
|
||||
func NewHTTPServer(lc fx.Lifecycle, log *zap.Logger, mux *http.ServeMux, cfg *Config) *http.Server {
|
||||
srv := &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
ln, err := net.Listen("tcp", srv.Addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen %s: %w", srv.Addr, err)
|
||||
}
|
||||
go func() {
|
||||
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Error("server error", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
log.Info("listening", zap.String("addr", srv.Addr))
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
log.Info("shutting down")
|
||||
return srv.Shutdown(ctx)
|
||||
},
|
||||
})
|
||||
return srv
|
||||
}
|
||||
```
|
||||
|
||||
## Background worker with graceful drain
|
||||
|
||||
```go
|
||||
type Worker struct {
|
||||
log *zap.Logger
|
||||
queue chan Job
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewWorker(lc fx.Lifecycle, log *zap.Logger) *Worker {
|
||||
w := &Worker{
|
||||
log: log,
|
||||
queue: make(chan Job, 100),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
go w.run()
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
close(w.queue) // signal "no more jobs"
|
||||
select {
|
||||
case <-w.done:
|
||||
w.log.Info("worker drained cleanly")
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
w.log.Warn("worker stop timeout")
|
||||
return ctx.Err()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Worker) run() {
|
||||
defer close(w.done)
|
||||
for job := range w.queue {
|
||||
job.Do(w.log)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The worker honors the stop context — under a 30-second `fx.StopTimeout` it has 30 seconds to drain. Beyond that, fx reports the timeout and the process exits.
|
||||
|
||||
## Multiple implementations of the same interface
|
||||
|
||||
Use named annotations + `fx.As` to register two `Cache` implementations and inject them by name:
|
||||
|
||||
```go
|
||||
fx.Provide(
|
||||
fx.Annotate(
|
||||
NewRedisCache,
|
||||
fx.As(new(Cache)),
|
||||
fx.ResultTags(`name:"redis"`),
|
||||
),
|
||||
fx.Annotate(
|
||||
NewMemcachedCache,
|
||||
fx.As(new(Cache)),
|
||||
fx.ResultTags(`name:"memcached"`),
|
||||
),
|
||||
)
|
||||
|
||||
type ServiceParams struct {
|
||||
fx.In
|
||||
Primary Cache `name:"redis"`
|
||||
Fallback Cache `name:"memcached"`
|
||||
}
|
||||
```
|
||||
|
||||
## fx.Supply for config and secrets
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cfg := mustLoadConfig() // parsed flags + env, before fx
|
||||
secret := os.Getenv("API_KEY")
|
||||
|
||||
fx.New(
|
||||
fx.Supply(cfg), // *Config available everywhere
|
||||
fx.Supply(fx.Annotate(secret, fx.ResultTags(`name:"apikey"`))),
|
||||
|
||||
fx.Provide(NewLogger, NewAPIClient),
|
||||
fx.Invoke(run),
|
||||
).Run()
|
||||
}
|
||||
|
||||
func NewAPIClient(cfg *Config, p struct {
|
||||
fx.In
|
||||
APIKey string `name:"apikey"`
|
||||
}) *APIClient {
|
||||
return &APIClient{baseURL: cfg.APIBaseURL, key: p.APIKey}
|
||||
}
|
||||
```
|
||||
|
||||
`fx.Supply` makes pre-built values first-class graph members. It is shorter and clearer than `fx.Provide(func() *Config { return cfg })`.
|
||||
|
||||
## Module-scoped decorator
|
||||
|
||||
```go
|
||||
var WorkerModule = fx.Module("worker",
|
||||
fx.Provide(NewWorker, NewJobQueue),
|
||||
// Inside this module, *zap.Logger is automatically named "worker".
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("worker")
|
||||
}),
|
||||
)
|
||||
|
||||
var APIModule = fx.Module("api",
|
||||
fx.Provide(NewServer, NewRouter),
|
||||
fx.Decorate(func(log *zap.Logger) *zap.Logger {
|
||||
return log.Named("api")
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
The two modules see different loggers — there is no shared mutation of the parent value.
|
||||
|
||||
## Optional dependency for tracing
|
||||
|
||||
```go
|
||||
type ServerParams struct {
|
||||
fx.In
|
||||
|
||||
Logger *zap.Logger
|
||||
Tracer trace.Tracer `optional:"true"`
|
||||
}
|
||||
|
||||
func NewServer(p ServerParams) *Server {
|
||||
s := &Server{log: p.Logger}
|
||||
if p.Tracer == nil {
|
||||
s.tracer = trace.NewNoopTracerProvider().Tracer("noop")
|
||||
} else {
|
||||
s.tracer = p.Tracer
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
Reach for `optional` only when the dependency is genuinely optional. A missing core service hidden behind `optional` becomes a nil-pointer panic at first use.
|
||||
|
||||
## Manual lifecycle for embedding fx in a CLI
|
||||
|
||||
When fx is one component inside a larger program (a CLI tool, a test runner), drive Start/Stop yourself instead of calling `Run()`:
|
||||
|
||||
```go
|
||||
func runFxApp(parent context.Context) error {
|
||||
app := fx.New(
|
||||
fx.Provide(NewConfig, NewLogger, NewWorker),
|
||||
fx.Invoke(func(*Worker) {}),
|
||||
)
|
||||
if err := app.Err(); err != nil {
|
||||
return fmt.Errorf("wire: %w", err)
|
||||
}
|
||||
|
||||
startCtx, cancel := context.WithTimeout(parent, 30*time.Second)
|
||||
defer cancel()
|
||||
if err := app.Start(startCtx); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-parent.Done():
|
||||
case <-app.Done(): // SIGINT/SIGTERM
|
||||
}
|
||||
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
return app.Stop(stopCtx)
|
||||
}
|
||||
```
|
||||
|
||||
`app.Err()` validates wiring without starting — useful for `--check` style flags.
|
||||
|
||||
## Custom event logger that filters noise
|
||||
|
||||
```go
|
||||
type ProductionLogger struct {
|
||||
inner *fxevent.ZapLogger
|
||||
}
|
||||
|
||||
func (l *ProductionLogger) LogEvent(e fxevent.Event) {
|
||||
switch e.(type) {
|
||||
case *fxevent.Provided, *fxevent.Supplied, *fxevent.Decorated:
|
||||
return // drop the per-Provide chatter
|
||||
default:
|
||||
l.inner.LogEvent(e)
|
||||
}
|
||||
}
|
||||
|
||||
fx.New(
|
||||
fx.Provide(NewZapLogger),
|
||||
fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
|
||||
return &ProductionLogger{inner: &fxevent.ZapLogger{Logger: log}}
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
In production, filtering provide/decorate noise leaves only lifecycle (start/stop) events and errors — much easier to audit.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Testing with uber-go/fx
|
||||
|
||||
`go.uber.org/fx/fxtest` integrates fx applications with `*testing.T`: errors fail the test instead of crashing the process, and lifecycle teardown is registered automatically.
|
||||
|
||||
## Pulling a value out of the graph with `fx.Populate`
|
||||
|
||||
```go
|
||||
func TestUserService_Create(t *testing.T) {
|
||||
var svc *UserService
|
||||
|
||||
app := fxtest.New(t,
|
||||
fx.Provide(
|
||||
func() Database { return &fakeDatabase{} },
|
||||
NewUserService,
|
||||
),
|
||||
fx.Populate(&svc),
|
||||
)
|
||||
defer app.RequireStop()
|
||||
app.RequireStart()
|
||||
|
||||
require.NoError(t, svc.Create(context.Background(), "alice@example.com"))
|
||||
}
|
||||
```
|
||||
|
||||
`fx.Populate(&svc)` fills `svc` with the value the graph would resolve. It replaces ad-hoc `fx.Invoke(func(s *UserService) { svc = s })` patterns.
|
||||
|
||||
## `fx.Replace` to swap a real dependency for a fake
|
||||
|
||||
```go
|
||||
func TestServer_HandlesDBError(t *testing.T) {
|
||||
var srv *http.Server
|
||||
fakeDB := &erroringDatabase{}
|
||||
|
||||
app := fxtest.New(t,
|
||||
ProductionModule, // the real wiring
|
||||
fx.Replace(fx.Annotate(fakeDB, fx.As(new(Database)))),
|
||||
fx.Populate(&srv),
|
||||
)
|
||||
defer app.RequireStop()
|
||||
app.RequireStart()
|
||||
|
||||
// Drive the server with a fake DB
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/users", nil)
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusInternalServerError, rec.Code)
|
||||
}
|
||||
```
|
||||
|
||||
`fx.Replace` works even when the original provider is buried inside a module — it overrides the resolved type without rewriting the module.
|
||||
|
||||
## Standalone lifecycle for a unit test
|
||||
|
||||
`fxtest.NewLifecycle(t)` gives you an `fx.Lifecycle` outside the `fx.New` machinery, useful for testing a single constructor that registers hooks:
|
||||
|
||||
```go
|
||||
func TestWorker_StartStop(t *testing.T) {
|
||||
lc := fxtest.NewLifecycle(t)
|
||||
|
||||
worker := NewWorker(lc, zaptest.NewLogger(t))
|
||||
require.NotNil(t, worker)
|
||||
|
||||
lc.RequireStart() // runs OnStart hooks
|
||||
require.True(t, worker.IsRunning())
|
||||
|
||||
lc.RequireStop() // runs OnStop hooks
|
||||
require.False(t, worker.IsRunning())
|
||||
}
|
||||
```
|
||||
|
||||
This is the lightest test for a constructor — no full graph, no `fx.New`.
|
||||
|
||||
## Asserting wire-time errors
|
||||
|
||||
```go
|
||||
func TestWiring_MissingDependency(t *testing.T) {
|
||||
app := fx.New(
|
||||
fx.Provide(NewServer), // depends on *sql.DB which is not provided
|
||||
fx.NopLogger,
|
||||
)
|
||||
require.Error(t, app.Err())
|
||||
require.Contains(t, app.Err().Error(), "missing type: *sql.DB")
|
||||
}
|
||||
```
|
||||
|
||||
Use `fx.New` (not `fxtest.New`) when you *expect* the wiring to fail — `fxtest.New` would call `t.Fatal`.
|
||||
|
||||
## Validating the production graph in CI
|
||||
|
||||
```go
|
||||
func TestProductionGraph(t *testing.T) {
|
||||
app := fx.New(
|
||||
ProductionOptions(), // every fx.Provide / fx.Module the binary uses
|
||||
fx.NopLogger,
|
||||
)
|
||||
require.NoError(t, app.Err())
|
||||
}
|
||||
```
|
||||
|
||||
`fx.New` validates the type graph without starting. The test fails before deploy on any missing-provider, cycle, or annotation mismatch.
|
||||
|
||||
## Test logger that captures fx events
|
||||
|
||||
When you want to assert on lifecycle behavior, route fx events into an in-memory observer:
|
||||
|
||||
```go
|
||||
// go.uber.org/zap/zaptest/observer
|
||||
core, recorded := observer.New(zap.InfoLevel)
|
||||
log := zap.New(core)
|
||||
|
||||
app := fxtest.New(t,
|
||||
fx.WithLogger(func() fxevent.Logger {
|
||||
return &fxevent.ZapLogger{Logger: log}
|
||||
}),
|
||||
fx.Provide(NewWorker),
|
||||
fx.Invoke(func(*Worker) {}),
|
||||
)
|
||||
defer app.RequireStop()
|
||||
app.RequireStart()
|
||||
|
||||
require.NotEmpty(t, recorded.FilterMessage("OnStart hook executed").All())
|
||||
```
|
||||
|
||||
## Testing a lifecycle hook in isolation
|
||||
|
||||
If a constructor returns a value *and* registers a hook, you often want to test both halves:
|
||||
|
||||
```go
|
||||
func TestNewServer_OnStartFailsBindError(t *testing.T) {
|
||||
// Bind a port so :0 is unavailable... no, simpler: pre-bind and pass that addr
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
addr := listener.Addr().String()
|
||||
|
||||
cfg := &Config{Addr: addr}
|
||||
lc := fxtest.NewLifecycle(t)
|
||||
|
||||
NewHTTPServer(lc, zaptest.NewLogger(t), cfg)
|
||||
|
||||
// Use Start directly (not RequireStart) so we can assert the error.
|
||||
require.Error(t, lc.Start(context.Background()))
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user