Uber 开源的 FX 可以帮助 Go 应用解耦依赖,实现更好的代码复用。原文: How to build large Golang applications using FX
Golang 是一种流行编程语言,功能强大,但人们还是会发现在处理依赖关系的同时组织大型代码库很复杂。
Go 开发人员有时必须将依赖项的引用传递给其他人,从而造成重用代码很困难,并造成技术(如数据库引擎或 HTTP 服务器)与业务代码紧密耦合。
FX 是由 Uber 创建的依赖注入工具,可以帮助我们避免不正确的模式,比如包中的init函数和传递依赖的全局变量,从而有助于重用代码。
本文将通过创建一个示例 web 应用,使用 FX 处理文本片段,以避免 Golang 代码中的紧耦合。
代码结构
首先定义代码结构:
 lib/  config/  db/  http/
config.ymlmain.go
utils/
       复制代码
 
该架构将应用程序的不同参与者分成自己的 Go 包,这样如果需要替换 DB 技术就会很有帮助。
每个包定义向其他包公开的接口及其实现。main.go文件将是依赖项的主要注入点,并将运行应用程序。
最后,utils包将包含将在应用程序中重用的所有不依赖于依赖项的代码片段。
首先,编写一个基本的main.go文件:
 package main
import "go.uber.org/fx"
func main() { app := fx.New() app.Run()}
       复制代码
 
声明 FX 应用程序并运行。接下来我们将看到如何在这个应用程序中注入更多特性。
模块架构
为了给应用程序添加功能,我们将使用 FX 模块,通过它在代码库中创建边界,使代码更具可重用性。
我们从配置模块开始,包含以下文件:
 // lib/config/config.gopackage config
type httpConfig struct { ListenAddress string}
type dbConfig struct { URL string}
type Config struct { HTTP httpConfig DB   dbConfig}
       复制代码
 
第一个文件定义了配置对象的结构。
 // lib/config/load.gopackage config
import ( "fmt" "github.com/spf13/viper")
func getViper() *viper.Viper { v := viper.New() v.AddConfigPath(".") v.SetConfigFile("config.yml") return v}
func NewConfig() (*Config, error) { fmt.Println("Loading configuration") v := getViper() err := v.ReadInConfig() if err != nil {  return nil, err } var config Config err = v.Unmarshal(&config) return &config, err}
       复制代码
 
load.go文件使用Viper框架从 YML 文件加载配置。我还添加了示例打印语句,以便稍后解释。
 // lib/config/fx.gopackage config
import "go.uber.org/fx"
var Module = fx.Module("config", fx.Provide(NewConfig))
       复制代码
 
这里通过使用fx.Module发布 FX 模块,这个函数接受两种类型的参数:
第一个参数是用于日志记录的模块的名称。
其余参数是希望向应用程序公开的依赖项。
这里我们只使用fx.Provide导出Config对象,这个函数告诉 FX 使用NewConfig函数来加载配置。
值得注意的是,如果 Viper 加载配置失败,NewConfig也会返回错误。如果错误不是 nil, FX 将显示错误并退出。
第二个要点是,该模块不导出 Viper,而只导出配置实例,从而允许我们轻松的用任何其他配置框架替换 Viper。
加载模块
现在,要加载我们的模块,只需要将它传递给main.go中的fx.New函数。
 // main.gopackage main
import ( "fx-example/lib/config" "go.uber.org/fx")
func main() { app := fx.New(  config.Module, ) app.Run()}
       复制代码
 
当我们运行这段代码时,可以在日志中看到:
 [Fx] PROVIDE    *config.Config <= fx-example/lib/config.NewConfig() from module "config"...[Fx] RUNNING
       复制代码
 
FX 告诉我们成功检测到fx-example/lib/config.NewConfig()提供了我们的配置,但是没有在控制台中看到"Loading configuration"。因为 FX 只在需要时调用提供程序,我们没使用刚才构建的配置,所以 FX 不会加载。
我们可以暂时在fx.New中添加一行,看看是否一切正常。
 func main() { app := fx.New(  config.Module,  fx.Invoke(func(cfg *config.Config) {}), ) app.Run()}
       复制代码
 
我们添加了对fix.Invoke的调用,注册在应用程序一开始就调用的函数,这将是程序的入口,稍后将启动我们的 HTTP 服务器。
DB 模块
接下来我们使用 GORM(Golang ORM)编写 DB 模块。
 package db
import ( "github.com/izanagi1995/fx-example/lib/config" "gorm.io/driver/sqlite" "gorm.io/gorm")
type Database interface { GetTextByID(id int) (string, error) StoreText(text string) (uint, error)}
type textModel struct { gorm.Model Text string}
type GormDatabase struct { db *gorm.DB}
func (g *GormDatabase) GetTextByID(id int) (string, error) { var text textModel err := g.db.First(&text, id).Error if err != nil {  return "", err } return text.Text, nil}
func (g *GormDatabase) StoreText(text string) (uint, error) { model := textModel{Text: text} err := g.db.Create(&model).Error if err != nil {  return 0, err } return model.ID, nil}
func NewDatabase(config *config.Config) (*GormDatabase, error) { db, err := gorm.Open(sqlite.Open(config.DB.URL), &gorm.Config{}) if err != nil {  return nil, err } err = db.AutoMigrate(&textModel{}) if err != nil {  return nil, err } return &GormDatabase{db: db}, nil}
       复制代码
 
在这个文件中,首先声明一个接口,该接口允许存储文本并通过 ID 检索文本。然后用 GORM 实现该接口。
在NewDatabase函数中,我们将配置作为参数,FX 会在注册模块时自动注入。
 // lib/db/fx.gopackage db
import "go.uber.org/fx"
var Module = fx.Module("db", fx.Provide(  fx.Annotate(   NewDatabase,   fx.As(new(Database)),  ), ),)
       复制代码
 
与配置模块一样,我们提供了NewDatabase函数。但这一次需要添加一个 annotation。
这个 annotation 告诉 FX 不应该将NewDatabase函数的结果公开为*GormDatabase,而应该公开为Database接口。这再次允许我们将使用与实现解耦,因此可以稍后替换 Gorm,而不必更改其他地方的代码。
不要忘记在main.go中注册db.Module。
 // main.gopackage main
import ( "fx-example/lib/config" "fx-example/lib/db" "go.uber.org/fx")
func main() { app := fx.New(  config.Module,  db.Module, ) app.Run()}
       复制代码
 
现在我们有了一种无需考虑底层实现就可以存储文本的方法。
HTTP 模块
以同样的方式构建 HTTP 模块。
 // lib/http/server.gopackage http
import ( "fmt" "github.com/izanagi1995/fx-example/lib/db" "io/ioutil" stdhttp "net/http" "strconv" "strings")
type Server struct { database db.Database}
func (s *Server) ServeHTTP(writer stdhttp.ResponseWriter, request *stdhttp.Request) { if request.Method == "POST" {  bodyBytes, err := ioutil.ReadAll(request.Body)  if err != nil {   writer.WriteHeader(400)   _, _ = writer.Write([]byte("error while reading the body"))   return  }  id, err := s.database.StoreText(string(bodyBytes))  if err != nil {   writer.WriteHeader(500)   _, _ = writer.Write([]byte("error while storing the text"))   return  }  writer.WriteHeader(200)  writer.Write([]byte(strconv.Itoa(int(id)))) } else {  pathSplit := strings.Split(request.URL.Path, "/")  id, err := strconv.Atoi(pathSplit[1])  if err != nil {   writer.WriteHeader(400)   fmt.Println(err)   _, _ = writer.Write([]byte("error while reading ID from URL"))   return  }  text, err := s.database.GetTextByID(id)  if err != nil {   writer.WriteHeader(400)   fmt.Println(err)   _, _ = writer.Write([]byte("error while reading text from database"))   return  }  _, _ = writer.Write([]byte(text)) }}
func NewServer(db db.Database) *Server { return &Server{database: db}}
       复制代码
 
HTTP 处理程序检查请求是 POST 还是 GET 请求。如果是 POST 请求,将正文存储为文本,并将 ID 作为响应发送。如果是 GET 请求,则从查询路径中获取 ID 对应的文本。
 // lib/http/fx.gopackage http
import ( "go.uber.org/fx" "net/http")
var Module = fx.Module("http", fx.Provide( fx.Annotate(  NewServer,  fx.As(new(http.Handler)), ),))
       复制代码
 
最后,将服务器公开为 http.Handler,这样就可以用更高级的工具(如 Gin 或 Gorilla Mux)替换刚才构建的简单 HTTP 服务器。
现在,我们可以将模块导入到main函数中,并编写一个Invoke调用来启动服务器。
 // main.gopackage main
import ( "fx-example/lib/config" "fx-example/lib/db" "fx-example/lib/http" "go.uber.org/fx" stdhttp "net/http")
func main() { app := fx.New(  config.Module,  db.Module,  http.Module,  fx.Invoke(func(cfg *config.Config, handler stdhttp.Handler) error {   go stdhttp.ListenAndServe(cfg.HTTP.ListenAddress, handler)   return nil  }), ) app.Run()}
       复制代码
 
瞧!我们有一个简单的 HTTP 服务器连接到一个 SQLite 数据库,所有都基于 FX。
总结一下,FX 可以帮助我们解耦代码,使其更易于重用,并且减少对正在进行的实现的依赖,还有助于更好的理解整体体系架构,而无需梳理复杂的调用和引用链。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
评论