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.yml
main.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.go
package config
type httpConfig struct {
ListenAddress string
}
type dbConfig struct {
URL string
}
type Config struct {
HTTP httpConfig
DB dbConfig
}
复制代码
第一个文件定义了配置对象的结构。
// lib/config/load.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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.go
package 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",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
评论