Go 框架学习之:go.uber.org/fx项目实战

背景

最近在开发一个边缘设备和应用的监控程序,作为一个长期使用Java开发的工程师,我对Spring生态有着深刻的印象------那种"只要你能想到的,它都可以帮你做到"的完善程度。这也许就是Java生态生生不息的重要原因之一。

转向Go语言开发后,我不由自主地思考:虽然Go生态没有Java那么丰富,但在依赖注入(DI)这样的基础需求上,应该也有优秀的解决方案。刚开始开发监控程序时,我认为这只是个小项目,暂时不需要引入第三方库,于是自己编写了一个简单的模块来手动管理对象的注册、注入和生命周期。

但随着业务功能不断增加,我发现自己编写的模块虽然还能应付,但长期来看项目维护成本会越来越大。要么继续维护和升级自己的模块,要么寻找成熟的第三方解决方案。最终,出于项目进度和代码质量的考虑,我发现了Uber开源的fx框架,并将其应用到了我的监控程序中。

我当前使用到的一些fx的特性

  1. 模块化设计 :通过fx.Module将系统拆分为多个独立的功能模块
  2. 依赖注入 :使用fx.Provide声明依赖关系,自动解决组件间的依赖
  3. 生命周期管理 :通过fx.Lifecyclefx.Invoke管理组件的启动和关闭顺序
  4. 分组注入 :利用group标签实现处理器的动态发现和注册
  5. 接口绑定 :使用fx.As将具体实现绑定到接口
  6. 注解功能 :通过fx.Annotate实现更灵活的依赖配置

具体使用过程

库的拉取和引入

go 复制代码
// 在我们项目中,go.mod同目录下执行
go get go.uber.org/fx

// 拉取完成后,go.mod 就会帮我们在项目中自动引入这个库了
require (
    go.uber.org/fx v1.24.0
    // ... 其他依赖
)

模块的声明

项目中,我采用了分层模块化设计,所以每个功能域都有独立的module.go文件。比如事件模块:

go:internal/infra/event/module.go 复制代码
// 这个变量就是使用fx声明一个模块
// 在这里,声明了"event"模块,并且使用fx.Provide函数提供了一个NewNotifier的构造方法
// 这个构造方法产生的就是我们要注入fx的对象
var Module = fx.Module("event",
    fx.Provide(NewNotifier),
)

// NewNotifier函数如下:
// 它返回一个EvtNotifier的接口对象
// 函数返回值其实可以是:接口对象、结构体对象(如果是结构体就返回指针类型)等等
// 这里我返回的是EvtNotifier接口对象,函数生产的是实现了这个接口的notifier结构体对象
func NewNotifier() EvtNotifier {
	return &notifier{
		subscribersMap: make(map[string][]*subscriberWrapper),
	}
}

这种设计可以让每个模块职责单一,便于后期的测试和维护。目前我的项目中共有十几个这样的模块,涵盖了配置管理、数据库操作、消息通信、任务处理等模块。

生命周期管理

fx的生命周期管理功能可以优雅地处理资源的初始化和清理。比如在数据库模块中,我这样做:

go:internal/infra/db/module.go 复制代码
// 当我们模块声明后,就可以使用fx.Invoke优雅地处理资源的初始化和清理
// 在数据库这个基础模块中,当数据库连接和对象都初始化之后,就可以做一些我们想做的事情
// 这里我想做的事情就是:进行数据库的Migration,即数据库结构或者数据合并操作(你可以认为是数据库变更操作)
// 所以在fx.Invoke中,通过我已经声明了的MigrationManager接口,进行数据库合并这个操作
// 当项目启动的时候将数据库合并的操作自动化完成,而不需要我们再另外手动修改数据库表结构等内容
// 至于数据库合并应该怎么做,后续我会再写一篇文章详细说明
fx.Invoke(func(lc fx.Lifecycle, migrator MigrationManager) {
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            return migrator.DoMigration(ctx)
        },
    })
}),



// 完整的数据库模块声明如下:
var Module = fx.Module("db", fx.Provide(
	NewService,
	NewTransactionManager,
	NewMigrationManager,
),
	fx.Invoke(func(lc fx.Lifecycle, migrator MigrationManager, dbSvc DB) {
		lc.Append(fx.Hook{
			OnStart: func(ctx context.Context) error {
				return m.DoMigration(ctx)
			},
			OnStop: func(ctx context.Context) error {
				return dbSvc.Close()
			},
		})
	}),
)

这样确保了数据库迁移在应用启动时自动执行,避免了手动调用的繁琐和可能出现的错误。

高级特性

分组注入

在Kubernetes资源采集模块中,我使用分组注入批量注册了26种资源处理器:

go:internal/kube/module.go 复制代码
// fx.Annotate声明多种资源类型的具体Handler构造方法 、Handler实现的共同接口以及Handler的所属组 
// 比如在这里,NewNamespaceHandler就是一个命名空间这个资源的构造函数,
// 它实现了`domain.Handler`这个接口,被标记的组是`resource_handlers`
// 其他资源也是相同的声明方式,实现的效果就是:一个共同的接口,多种资源实现类
var HandlerModule = fx.Provide(
	fx.Annotate(
		handlers.NewNamespaceHandler,
		fx.As(new(domain.Handler)),
		fx.ResultTags(`group:"resource_handlers"`),
	),
	fx.Annotate(
		handlers.NewNodeHandler,
		fx.As(new(domain.Handler)),
		fx.ResultTags(`group:"resource_handlers"`),
	),
        ...... // 其他资源类型的Handler
)

// 实现具体接口的自动注入
var Module = fx.Module("kube",
        //  使用上面声明的资源Handler
	HandlerModule, 
        // 将资源组提供出来
	fx.Provide(fx.Annotate(            
		func(handlers ...domain.Handler) []domain.Handler {
			return handlers
		},
		fx.ParamTags(`group:"resource_handlers"`),
	)),
        // 由于每个资源都实现了同一个接口domain.Handler,所以必须实现接口函数Kind()
        // 每种资源类型的Kind()函数都返回了各自定义的类型,如 Namespace、Deployment等
        // 这里就可以为app.NewService实现一个自动注入 所有资源Handler 的方式
	fx.Provide(func(handlers []domain.Handler, config *config.Config,
		probe *probe.Probe, mqttCli mqtt.Client) domain.Service {
		handlerMap := make(map[string]domain.Handler)
		for _, h := range handlers {
			handlerMap[h.Kind()] = h
		}
		return app.NewService(mqttCli, config, probe, handlerMap)
	}),
)

// 当我们要使用Handler的时候,只需要

handler := s.handlerMap[kind]
data := handler.GetData()

// 是不是很优雅,哈哈,跟 Spring 的效果一样了

这种设计让新增资源处理器变得非常简单,只需要添加一个新的Provide声明即可。

接口绑定

在命令处理模块中,我将服务注册为MQTT订阅者:

go:internal/command/module.go 复制代码
// 这里跟上面的kube模块其实是一样的
fx.Provide(
    fx.Annotate(
        NewService,
        fx.As(new(mqtt.TopicSubscriber)),
        fx.ResultTags(`group:"mqtt_subscribers"`),
    ),
),

这样实现了接口与实现的解耦,提高了代码的灵活性。

任务处理器自动注册

在任务处理模块中,我实现了处理器的自动发现和映射:

go:internal/task/module.go 复制代码
// 这里跟上面的kube模块其实是一样的
fx.Invoke(func(handlers []ActionHandler, param struct {
    fx.In
    Handlers []ActionHandler `group:"action_handlers"`
}) {
    handlerMap := make(map[string]ActionHandler)
    for _, handler := range param.Handlers {
        handlerMap[handler.ActionType()] = handler
    }
    // 注册到全局管理器
}),

在main函数中使用

使用fx.New()创建应用实例:

go:main.go 复制代码
app := fx.New(
    fx.Module("probe",
        event.Module,         // 事件模块
        config.Module,        // 配置模块
        logger.Module,        // 日志模块
        db.Module,            // 数据库模块
        mqtt.Module,          // MQTT模块
        web.Module,           // Web模块
        file.Module,          // 文件模块
        kube.Module,          // Kubernetes模块
        ....                // 引入其他模块
    ),
    // 勾子方法
    fx.Invoke(func(lc fx.Lifecycle) {
        lc.Append(fx.Hook{
            OnStart: func(ctx context.Context) error {
                // 同样可以在这里进行资源的管理,每个模块中也可以自定义
                return nil
            },
            OnStop: func(ctx context.Context) error {
                // 程序退出的时候会调用这里的方法代码,每个模块中也可以自定义
                return nil
            },
        })
    }),
)

这种设计让main函数变得非常简洁,所有的模块依赖和生命周期管理都交给了fx框架处理。

总结

通过在实际项目中使用fx框架,我能感受到fx在Go语言依赖注入和生命周期管理方面的强大能力。与之前自己手动实现的(DI)模块相比,fx提供了以下优势:

  1. 标准化:遵循社区标准,降低了新团队成员的学习成本
  2. 功能丰富:提供了分组注入、接口绑定、生命周期管理等高级特性
  3. 可扩展性:模块化设计让系统更容易扩展和维护
  4. 类型安全:充分利用Go的强类型特性,减少运行时错误
  5. 测试友好:依赖注入让单元测试和集成测试更加容易(这篇文章中没有涉及到,后续会有)

以上就是我在项目中使用fx这个库的一些简单总结,希望对要使用和学习它的一些同僚有用。文章中,很多关键信息都写在了代码的注解里面,可以加强对每个方法的印象。

相关推荐
小蒜学长3 小时前
django全国小米su7的行情查询系统(代码+数据库+LW)
java·数据库·spring boot·后端
听风同学5 小时前
RAG的灵魂-向量数据库技术深度解析
后端·架构
橙序员小站5 小时前
搞定系统面试题:如何实现分布式Session管理
java·后端·面试
老青蛙5 小时前
权限系统设计-功能设计
后端
粘豆煮包5 小时前
脑抽研究生Go并发-1-基本并发原语-下-Cond、Once、Map、Pool、Context
后端·go
IT_陈寒6 小时前
Vite5.0性能翻倍秘籍:7个极致优化技巧让你的开发体验飞起来!
前端·人工智能·后端
Edward.W6 小时前
用 Go + HTML 实现 OpenHarmony 投屏(hdckit-go + WebSocket + Canvas 实战)
开发语言·后端·golang
南囝coding6 小时前
Claude 封禁中国?为啥我觉得是个好消息
前端·后端
六边形工程师6 小时前
Docker安装神通数据库ShenTong
后端