返回 Skills
samber/cc-skills-golang· MIT 内容可用

golang-uber-fx

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.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


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.1.3" 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 Bash(godig:) Bash(gopls:) LSP mcp__gopls__

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:

This skill is not exhaustive. Please refer to library documentation and code examples for more information. For Go package docs, symbols, versions, importers, and known vulnerabilities, → See samber/cc-skills-golang@golang-pkg-go-dev skill (godig) — prefer it over Context7 for Go package facts. To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See samber/cc-skills-golang@golang-gopls skill (gopls). Context7 remains a fallback for docs not indexed on pkg.go.dev.

go get go.uber.org/fx

fx vs. dig

fx is built on top of dig and shares the same reflection-based container engine. The DI primitives (Provide, Invoke, In/Out structs, named values, value groups) are identical — fx.In/fx.Out are re-exports of dig.In/dig.Out.

What fx adds on top:

Concerndigfx
DI containerdig.New()✅ (embedded)
Lifecycle hooksfx.Lifecycle OnStart/OnStop
Module systemfx.Module with scoped decorators
Signal-aware run loopapp.Run() blocks on SIGINT/SIGTERM
Structured event loggingfx.WithLogger / fxevent
Startup/shutdown timeoutfx.StartTimeout / fx.StopTimeout

Choose fx for long-running services (HTTP servers, workers, daemons) — lifecycle and signal handling are mandatory there, and modules make large service graphs manageable.

Choose raw dig when you need wiring without a framework: CLI tools, libraries that expose a container to callers, test harnesses, or embedding DI into an existing app that manages its own lifecycle. See samber/cc-skills-golang@golang-uber-dig skill.

The Application

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

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.

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):

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.

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:

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:

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.

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.

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

MistakeFix
Long-running work directly in OnStartSpawn a goroutine inside OnStart; the hook itself must return quickly so dependent hooks can run.
fx.Provide something that should be fx.SupplyPre-built values (config, secrets) belong in fx.Supply — clearer and avoids a no-op constructor.
Module decorator leaking to siblingsDecorate inside fx.Module(...) — decorators flow only to descendants. A top-level fx.Decorate is global.
Group order assumedGroups are unordered. If order matters, provide an ordered slice from one constructor.
Constructors with side effectsSide effects belong in OnStart — constructors should be cheap and pure-ish, since they may run concurrently and lazily.
Forgotten fx.InvokeWithout 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.

Further Reading

  • advanced.md — Supply/Replace/Decorate, optional deps, custom event logging, manual lifecycle, full Quick Reference
  • 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 — 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.

附带文件

evals/evals.json
[
  {
    "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"}
    ]
  }
]
references/advanced.md
# 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 |
references/recipes.md
# 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.
references/testing.md
# 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()))
}
```
    golang-uber-fx | Prompt Minder