写点什么

基于 FX 构建大型 Golang 应用

作者:俞凡
  • 2023-11-19
    上海
  • 本文字数:4000 字

    阅读完需:约 13 分钟

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 模块,通过它在代码库中创建边界,使代码更具可重用性。


我们从配置模块开始,包含以下文件:


  • config.go定义向应用程序公开的数据结构。

  • fx.go将模块发布,设置需要的一切,并在启动时加载配置。

  • load.go是接口的实现。


// 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",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 5
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
基于FX构建大型Golang应用_golang_俞凡_InfoQ写作社区