基於 FX 構建大型 Golang 應用
Uber 開源的 FX 可以幫助 Go 應用解耦依賴,實現更好的代碼複用。原文: How to build large Golang applications using FX[1]
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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ha0DkTOxUwexTV-c_sx1jg