首页 > 其他分享 >基于fx构建大型golang应用

基于fx构建大型golang应用

时间:2024-01-31 09:12:54浏览次数:30  
标签:nil err fx db golang FX 构建 config

基于FX构建大型Golang应用

原创 俞凡 DeepNoMind DeepNoMind 2023-12-23 13:29 发表于上海 1人听过

Uber开源的FX可以帮助Go应用解耦依赖,实现更好的代码复用。原文: How to build large Golang applications using FX[1]

构建复杂的Go应用程序可能会引入很多耦合

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

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

  • config.go定义向应用程序公开的数据结构。
  • fx.go将模块发布,设置需要的一切,并在启动时加载配置。
  • load.go是接口的实现。
// 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",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

参考资料

[1]

How to build large Golang applications using FX: https://medium.com/@devops-guy/how-to-build-large-golang-applications-using-fx-3ccfac153fc1

- END -

 

俞凡 DeepNoMind

 感恩有你,一路同行 

赞赏二维码喜欢作者

1人喜欢 Golang47 架构192 大厂实践88 Golang · 目录 上一篇Go、容器以及Linux调度器下一篇Golang不可不知的7个并发概念 阅读原文 阅读 2588 DeepNoMind ​ 喜欢此内容的人还喜欢   Go事件管理器:简单实现     我看过的号 DeepNoMind 不看的原因   Go: 探索TinyGo在微控制器和IoT中发挥力量     我关注的号 运维开发王义杰 不看的原因   浅谈Golang中的defer语法及其机制     我看过的号 程序员的碎碎念 不看的原因   关注公众号后可以给作者发消息              

人划线

 

标签:nil,err,fx,db,golang,FX,构建,config
From: https://www.cnblogs.com/cheyunhua/p/17998494

相关文章

  • dockerfile安装jenkins 并配置构建工具(node、npm、maven、git)
    dockerfile安装jenkins并配置构建工具(node、npm、maven、git):https://blog.csdn.net/weixin_39660224/article/details/88775707?ops_request_misc=&request_id=&biz_id=102&utm_term=dockerfile%20%E5%88%9B%E5%BB%BAjenkins&utm_medium=distribute.pc_search_result.......
  • 利用Jenkins自动构建并部署项目
    利用Jenkins自动构建并部署项目:https://blog.csdn.net/li532788/article/details/88031076?ops_request_misc=&request_id=&biz_id=102&utm_term=sudo%20rpm%20--import%20https://pkg.&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~defaul......
  • gn构建工具
    参考链接:webrtc的GN构建系统  https://zhuanlan.zhihu.com/p/631038559 gn项目构建工具学习记录  https://blog.51cto.com/harmonyos/6030968Chromium GN构建工具的使用 https://www.cnblogs.com/bigben0123/p/12626012.htmlgn/ninja:谷歌的新一代项目构建系统简介 ......
  • 使用Golang实现ping检测主机在线的功能
    使用"github.com/go-ping/ping"这个第三方库可以非常简单的实现ping功能packagemainimport("fmt""os""time""github.com/go-ping/ping")funcCheckHostOnline(ipaddrstring)bool{pinger,err:=ping.N......
  • 构建知识图谱:从技术到实战的完整指南
    本文深入探讨了知识图谱的构建全流程,涵盖了基础理论、数据获取与预处理、知识表示方法、知识图谱构建技术等关键环节。关注TechLead,分享AI全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师......
  • 《构建之法》读后感2
    《构建之法》读后感2《构建之法》是一本引人深思的书籍,它通过深入剖析构建的本质,为读者提供了一种独特的思考方式和实践方法。在阅读过程中,我深受启发,产生了许多感悟。首先,书中强调了构建的重要性。构建不仅仅是建筑物或系统的组装,更是一种创造性的过程,是对问题的解决和创新的实......
  • 《构建之法读后感》
    《构建之法读后感》这是老师寒假给我们推荐的书籍,老师曾经说过,作为一个合格的程序员首先要学会读书,从书中去学会知识,总结书中的经验,为自己所用。这是一个优秀程序员的必备素养。因此,我又读了《构建之法》这本书,并产生了很多知识和自身的体会。刚开始读《构建之法》这本书时,书上......
  • 构建之法的读书笔记与读后感3
    团队和流程团队有一致的集体目标,团队要一起完成这目标。团队成员有各自的分工,互相依赖合作,共同完成任务。团队的集中工作模式(主治医师模式、明星模式、社区模式、业余剧团模式、秘密团队、特工团队、交响乐团模式、爵士乐模式、功能团队模式、官僚漠式)开发流程:(写了再改漠式、......
  • 《构建之法》读书笔记三
    第三章软件工程师的成长包括个人能力的衡量和发展和软件工程师的职业发展以及技能的反面。要想成为初级软件工程师必须具备软件开发方面的知识,提升技术技能、内核调试器的掌握;积累问题领域的知识与经验;对通用的软件设计思想和软件工程思想的理解;提升职业技能(多去学表达技能;与人沟......
  • 《构建之法》读书阅读笔记一
     第一章概论:1.“软件=程序+软件工程”问题:程序与软件的区别是什么?回答:以前我总是分不清何为程序,何为软件,一直以为比较完善的程序就是一个软件。于是,我上网查了资料,更加明确两者的区别:程序(program)是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合。为进......