Skip to content

依赖注入

fun 自带一个按类型装配的轻量 DI 容器(box.go):单例创建、递归装配、New() 生命周期钩子。不引入任何第三方依赖。

核心 API:Wired

go
// 按类型创建/获取单例;先注入其 fun:"auto" 字段,再调 New()(若有)
func Wired[T any]() (*T, error)
go
cfg, err := fun.Wired[config.Config]()
if err != nil {
    log.Fatal(err) // 装配失败:缺少可注入依赖等
}

Wired[T] 的执行顺序:

  1. 容器里已有 *T → 直接返回;
  2. 否则创建 &T{},扫描其字段,带 fun:"auto" 标签的指针结构体字段递归装配(容器没有就先创建依赖);
  3. T 实现了 New()必须无参无返回)则调用——连接类资源在这里初始化,失败可以 log.Fatalf
go
type Postgres struct {
    Config *Config `fun:"auto"` // 自动注入已注册的 *Config
    Pool   *sql.DB
}

func (p *Postgres) New() {
    pool, err := sql.Open("postgres", p.Config.DSN)
    if err != nil {
        log.Fatalf("postgres: %v", err) // 连接类资源失败即退出
    }
    p.Pool = pool
}

服务字段的自动装配

服务结构体上的指针结构体字段不需要标签BindService 注册时自动从容器解析或补建:

go
type UserSvc struct {
    fun.Ctx
    Pg    *platform.Postgres // 容器有则复用,无则递归创建(触发 New())
    Redis *platform.Redis
}

BindService 会拉起基础设施

BindService 触发真实装配——Postgres / Redis 的 New() 会真正建连接。因此代码生成不要用 BindService,用 BindServiceForGen(只登记元信息,不装配)。

启动顺序

go
func main() {
    // ① 先 Wired 基础配置与平台单例(触发 New() 建连接)
    cfg, err := fun.Wired[config.Config]()
    if err != nil {
        log.Fatal(err)
    }
    _, _ = fun.Wired[platform.Postgres]()
    _, _ = fun.Wired[platform.Redis]()

    // ② 再注册服务(服务字段从容器取已建好的单例)
    f := fun.GetFun()
    if err := f.BindService(&UserSvc{}); err != nil {
        log.Fatal(err)
    }

    // ③ 最后启动
    go f.Start(cfg.ListenPort())
}

顺序不强制(BindService 会自动补建缺失依赖),但显式先 Wired 基础设施可以让连接问题在启动最早、堆栈最清晰的地方暴露。

Guard 也是 Box

Guard 结构体同样参与装配,字段自动注入:

go
type AuthGuard struct {
    Redis *platform.Redis // 会话存储自动注入
}

func (g *AuthGuard) Guard(ctx fun.Ctx) error {
    token := ctx.State["token"]
    // ... 校验
    return nil
}

路由级 Guard(BindRoute 变参)亦然;Guard 依赖装配失败以 error 返回(不 panic)。

设计要点

问题答案
单例粒度*T 类型,一个类型一个实例
依赖声明Box 内 fun:"auto" 标签;服务/Guard 字段免标签
生命周期New() 无参无返回,装配完成即调用
失败策略Wired / BindService / BindRoute 返回 error;New()log.Fatalf
每请求服务实例每请求新建,依赖(Box)全局共享

下一步

基于 MIT 许可发布