90%的Go都在重復(fù)造輪子:一個(gè)泛型資源管理庫幫你終結(jié)連接池噩夢

90%的Go開發(fā)者都在重復(fù)造輪子:一個(gè)泛型資源管理庫幫你終結(jié)連接池噩夢

你有沒有遇到過這種情況?

項(xiàng)目里同時(shí)用了MySQL、Redis、MongoDB,每個(gè)都要寫一套初始化邏輯。配置散落在各個(gè)角落,關(guān)閉資源的時(shí)候漏掉一個(gè),內(nèi)存泄漏查半天。更崩潰的是,換個(gè)項(xiàng)目又得把這些代碼復(fù)制粘貼一遍。

今天介紹一個(gè)開源庫,用不到300行代碼,徹底解決資源管理的混亂問題。

image.png

一、核心設(shè)計(jì):三個(gè)概念搞定一切

這個(gè)庫的設(shè)計(jì)極其精簡,只有三個(gè)核心概念:

Opener —— 告訴庫怎么創(chuàng)建資源
Closer —— 告訴庫怎么銷毀資源
Manager —— 幫你統(tǒng)一管理所有資源

看一眼類型定義你就懂了:

type Opener[C any, T any] func(ctx context.Context, cfg C) (T, error)
type Closer[T any] func(ctx context.Context, val T) error

C是配置類型,T是資源類型。Go 1.18泛型的威力在這里體現(xiàn)得淋漓盡致——一套代碼管理所有類型的資源。

二、惰性初始化:不用不創(chuàng)建

很多人寫資源管理,喜歡在程序啟動時(shí)把所有連接都建好。數(shù)據(jù)庫連接池、Redis客戶端、各種SDK,一股腦全初始化。

問題來了:如果某個(gè)服務(wù)這次請求根本用不到MongoDB,為什么要提前建連接?

這個(gè)庫采用惰性初始化策略。你先注冊配置,資源在第一次Get的時(shí)候才真正創(chuàng)建

// 注冊只保存配置,不創(chuàng)建連接
group.Register(ctx, "main-db", dbConfig)
group.Register(ctx, "cache", redisConfig)

// 首次調(diào)用才真正建立連接
db, err := group.Get(ctx, "main-db")

這個(gè)設(shè)計(jì)帶來兩個(gè)好處:啟動速度快,資源按需分配。

三、并發(fā)安全:雙重檢查鎖的教科書實(shí)現(xiàn)

多個(gè)goroutine同時(shí)請求同一個(gè)資源怎么辦?

庫里用了經(jīng)典的雙重檢查鎖定模式。先用讀鎖快速判斷資源是否就緒,沒就緒再升級寫鎖創(chuàng)建。這樣既保證了并發(fā)安全,又避免了每次訪問都加重鎖的性能損耗。

// 讀鎖:快速路徑
g.m.mu.RLock()
if conn.ready {
    val := conn.val
    g.m.mu.RUnlock()
    return val, nil
}
g.m.mu.RUnlock()

// 寫鎖:慢速路徑,惰性創(chuàng)建
g.m.mu.Lock()
defer g.m.mu.Unlock()

// 二次檢查,防止重復(fù)創(chuàng)建
if conn.ready {
    return conn.val, nil
}

val, err := g.m.opener(ctx, conn.cfg)

這段代碼可以當(dāng)作Go并發(fā)編程的范本來學(xué)習(xí)。

四、分組管理:多租戶場景的救星

實(shí)際項(xiàng)目中,你可能需要管理多套同類型的資源。比如SaaS系統(tǒng)里,每個(gè)租戶一套數(shù)據(jù)庫配置。

這個(gè)庫原生支持分組:

manager := registry.New(dbOpener, dbCloser)

// 按租戶分組
manager.AddGroup("tenant-A")
manager.AddGroup("tenant-B")

// 獲取租戶A的數(shù)據(jù)庫
groupA, _ := manager.Group("tenant-A")
groupA.Register(ctx, "primary", tenantAConfig)
db, _ := groupA.Get(ctx, "primary")

每個(gè)Group獨(dú)立管理自己的資源,互不干擾。關(guān)閉時(shí)可以單獨(dú)關(guān)閉某個(gè)組,也可以一鍵關(guān)閉整個(gè)Manager。

五、優(yōu)雅關(guān)閉:告別資源泄漏

程序退出時(shí)忘記關(guān)閉連接,這種bug隱蔽又致命。

庫提供了統(tǒng)一的Close方法,自動遍歷所有已創(chuàng)建的資源并關(guān)閉:

// 關(guān)閉單個(gè)組
errs := group.Close(ctx)

// 關(guān)閉整個(gè)管理器
errs := manager.Close(ctx)

返回值是錯(cuò)誤切片,方便你知道哪些資源關(guān)閉失敗了。

六、實(shí)戰(zhàn):GORM接入完整示例

廢話不多說,直接上可運(yùn)行的代碼:

package main

import (
    "context"
    "fmt"

    "github.com/qq1060656096/bizutil/registry"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

// 數(shù)據(jù)庫配置
type DBConfig struct {
    DSN string
}

// 創(chuàng)建數(shù)據(jù)庫連接(opener)
func openDB(ctx context.Context, cfg DBConfig) (*gorm.DB, error) {
    return gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{})
}

// 關(guān)閉數(shù)據(jù)庫連接(closer)
func closeDB(ctx context.Context, db *gorm.DB) error {
    sqlDB, _ := db.DB()
    return sqlDB.Close()
}

func main() {
    ctx := context.Background()

    // 創(chuàng)建 registry(單組模式)
    DB := registry.NewGroup[DBConfig, *gorm.DB](
        openDB,
        closeDB,
    )

    // 注冊數(shù)據(jù)庫(此時(shí)不會真正連接)
    DB.Register(ctx, "main", DBConfig{
        DSN: "user:pass@tcp(127.0.0.1:3306)/test?parseTime=true",
    })

    // 第一次獲取時(shí)才創(chuàng)建連接
    db := DB.MustGet(ctx, "main")

    // 使用 GORM
    data := make(map[string]interface{})
    db.Raw("SELECT 'hello' AS demo").Scan(&data)
    fmt.Println(data) // map[demo:hello]

    // 程序退出時(shí)統(tǒng)一關(guān)閉
    DB.Close(ctx)
}

整個(gè)流程清晰明了:定義opener和closer → 創(chuàng)建registry → 注冊配置 → 按需獲取 → 統(tǒng)一關(guān)閉。

如果你的項(xiàng)目需要管理多套數(shù)據(jù)庫(比如讀寫分離、多租戶),用Manager模式:

manager := registry.New(openDB, closeDB)

// 按用途分組
manager.AddGroup("write")
manager.AddGroup("read")

// 注冊主庫
writeGroup, _ := manager.Group("write")
writeGroup.Register(ctx, "master", masterConfig)

// 注冊從庫
readGroup, _ := manager.Group("read")
readGroup.Register(ctx, "slave-1", slave1Config)
readGroup.Register(ctx, "slave-2", slave2Config)

// 寫操作用主庫
masterDB, _ := writeGroup.Get(ctx, "master")

// 讀操作用從庫
slaveDB, _ := readGroup.Get(ctx, "slave-1")

// 程序退出時(shí)一鍵關(guān)閉所有連接
defer manager.Close(ctx)

七、源碼亮點(diǎn)

翻了一遍源碼,有幾個(gè)細(xì)節(jié)值得學(xué)習(xí):

  1. 泛型約束用any:最大化靈活性,任何類型都能管理
  2. Closer可以為nil:有些資源不需要顯式關(guān)閉,設(shè)計(jì)上考慮到了
  3. 錯(cuò)誤類型豐富:ErrGroupNotFound、ErrResourceNotFound、ErrCloseResourceFailed,定位問題很方便
  4. Must系列方法:確定資源存在時(shí)可以用MustGet,代碼更簡潔

寫在最后

這個(gè)庫的代碼量不大,但設(shè)計(jì)思路值得借鑒。泛型讓Go的資源管理終于可以做到真正的通用化,不用再為每種資源寫重復(fù)的管理代碼。

倉庫地址:github.com/qq1060656096/bizutil/registry

如果你的項(xiàng)目里也有資源管理的痛點(diǎn),不妨試試。有問題歡迎在評論區(qū)交流。


覺得有用的話,點(diǎn)個(gè)贊收藏一下,我會持續(xù)分享Go語言的實(shí)用技巧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容