[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -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")
|
||||
```
|
||||
Reference in New Issue
Block a user