基於 FX 構建大型 Golang 應用

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 模塊,通過它在代碼庫中創建邊界,使代碼更具可重用性。

我們從配置模塊開始,包含以下文件:

// 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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ha0DkTOxUwexTV-c_sx1jg