[teamai] Push 87 resource(s) from XingfenD

This commit is contained in:
2026-09-10 16:10:45 +08:00
parent 425c9c078a
commit 65c04def51
1314 changed files with 211681 additions and 0 deletions
@@ -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()))
}
```