Files
teamai-test/.teamai/skills/common/golang-google-wire/references/recipes.md
T

265 lines
6.6 KiB
Markdown

# Recipes — google/wire
End-to-end examples. Each recipe is self-contained.
## HTTP Server with Postgres and Redis
A typical service: parsed config → DB (with cleanup) → Redis (with cleanup) → repo → service → HTTP server.
```
myapp/
├── config/
│ ├── config.go
│ └── wire.go
├── infra/
│ ├── db.go
│ ├── cache.go
│ └── wire.go
├── repo/
│ ├── user.go
│ └── wire.go
├── service/
│ ├── user.go
│ └── wire.go
├── transport/
│ ├── handler.go
│ └── wire.go
├── wire.go // injector — //go:build wireinject
├── wire_gen.go // generated — commit this
└── main.go
```
```go
// config/config.go
type Config struct {
Addr string
DSN string
CacheAddr string
}
func NewConfig() *Config {
return &Config{
Addr: env("ADDR", ":8080"),
DSN: mustEnv("DATABASE_URL"),
CacheAddr: env("REDIS_ADDR", "localhost:6379"),
}
}
// config/wire.go
var ConfigSet = wire.NewSet(NewConfig)
```
```go
// infra/db.go
func NewDB(cfg *config.Config) (*sql.DB, func(), error) {
db, err := sql.Open("postgres", cfg.DSN)
if err != nil { return nil, nil, err }
if err := db.Ping(); err != nil { db.Close(); return nil, nil, err }
return db, func() { db.Close() }, nil
}
// infra/cache.go
func NewRedis(cfg *config.Config) (*redis.Client, func(), error) {
c := redis.NewClient(&redis.Options{Addr: cfg.CacheAddr})
if err := c.Ping(context.Background()).Err(); err != nil {
return nil, nil, err
}
return c, func() { c.Close() }, nil
}
// infra/wire.go
var InfraSet = wire.NewSet(NewDB, NewRedis)
```
```go
// repo/user.go
type UserStore interface {
GetUser(ctx context.Context, id int64) (*User, error)
}
type PostgresUserRepo struct{ db *sql.DB }
func NewUserRepo(db *sql.DB) *PostgresUserRepo { return &PostgresUserRepo{db: db} }
// repo/wire.go
var RepoSet = wire.NewSet(
NewUserRepo,
wire.Bind(new(UserStore), new(*PostgresUserRepo)),
)
```
```go
// service/user.go
type UserService struct {
store repo.UserStore
cache *redis.Client
}
func NewUserService(store repo.UserStore, cache *redis.Client) *UserService {
return &UserService{store: store, cache: cache}
}
// service/wire.go
var ServiceSet = wire.NewSet(NewUserService)
```
```go
// wire.go
//go:build wireinject
package main
func InitApp() (*transport.Handler, func(), error) {
wire.Build(
config.ConfigSet,
infra.InfraSet,
repo.RepoSet,
service.ServiceSet,
transport.NewHandler,
)
return nil, nil, nil
}
```
```go
// main.go
func main() {
handler, cleanup, err := InitApp()
if err != nil { log.Fatal(err) }
defer cleanup()
srv := &http.Server{Addr: ":8080", Handler: handler}
log.Fatal(srv.ListenAndServe())
}
```
## Multiple Build Variants (Prod vs Dev)
Use separate injector files with `//go:build` constraints to select different provider sets at build time.
```go
// wire_prod.go
//go:build wireinject && !dev
package main
func InitApp() (*App, func(), error) {
wire.Build(ProdSet, NewApp)
return nil, nil, nil
}
// wire_dev.go
//go:build wireinject && dev
package main
func InitApp() (*App, func(), error) {
wire.Build(DevSet, NewApp) // DevSet swaps real DB for in-memory SQLite
return nil, nil, nil
}
```
Use `-output_file_prefix` to write separate output files — both commands would otherwise overwrite the same `wire_gen.go`:
```bash
wire -tags dev -output_file_prefix=wire_gen_dev gen .
wire -output_file_prefix=wire_gen_prod gen .
```
Add build constraints to the generated files so only one compiles per build:
```go
// wire_gen_prod.go — add at the top (after wire writes it)
//go:build !dev
// wire_gen_dev.go — add at the top
//go:build dev
```
Commit both generated files. At build time, only the matching file is compiled.
## Cleanup-Heavy Graph
When several providers need shutdown coordination, wire's reverse-order cleanup is essential.
```go
// Providers return (T, func(), error)
func NewDBPool(cfg *Config) (*pgxpool.Pool, func(), error) {
pool, err := pgxpool.New(context.Background(), cfg.DSN)
if err != nil { return nil, nil, err }
return pool, func() { pool.Close() }, nil
}
func NewOTelExporter(cfg *Config) (*otlptrace.Exporter, func(), error) {
exp, err := otlptracegrpc.New(context.Background(), ...)
if err != nil { return nil, nil, err }
return exp, func() { exp.Shutdown(context.Background()) }, nil
}
func NewTracerProvider(exp *otlptrace.Exporter) (*trace.TracerProvider, func(), error) {
tp := trace.NewTracerProvider(trace.WithBatcher(exp))
return tp, func() { tp.Shutdown(context.Background()) }, nil
}
```
Wire generates shutdown in reverse: `TracerProvider` → `OTelExporter` → `DBPool`. Each cleanup runs before its dependencies shut down — guaranteeing in-flight spans are flushed before the exporter closes.
## Embedding Wire in a CLI
Wire produces a struct, not an app framework. You control the lifecycle:
```go
// wire.go
//go:build wireinject
package cmd
func InitServer(cfg *Config) (*http.Server, func(), error) {
wire.Build(InfraSet, ServiceSet, NewHTTPServer)
return nil, nil, nil
}
// cmd/serve.go
func runServe(cfg *Config) error {
srv, cleanup, err := InitServer(cfg)
if err != nil { return err }
defer cleanup()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(ctx)
}
```
Unlike `fx.Run()`, wire does not manage the lifecycle loop. Implement signal handling and graceful shutdown explicitly. This is a feature for CLI tools that spin up short-lived services and need precise control over the shutdown sequence.
## Passing External Values to Wire
Values built before wire runs (parsed config, test doubles) become injector parameters:
```go
//go:build wireinject
// cfg is resolved outside the graph — treated as a provided *Config
func InitApp(cfg *Config) (*App, func(), error) {
wire.Build(InfraSet, ServiceSet, NewApp)
return nil, nil, nil
}
// main.go
cfg, err := config.Load()
if err != nil { log.Fatal(err) }
app, cleanup, err := InitApp(cfg)
```
The parameter `cfg *Config` satisfies any downstream provider that requests `*Config` — no `wire.Value` or extra set entry needed.