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行代碼,徹底解決資源管理的混亂問題。

一、核心設(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í):
- 泛型約束用any:最大化靈活性,任何類型都能管理
- Closer可以為nil:有些資源不需要顯式關(guān)閉,設(shè)計(jì)上考慮到了
- 錯(cuò)誤類型豐富:ErrGroupNotFound、ErrResourceNotFound、ErrCloseResourceFailed,定位問題很方便
- Must系列方法:確定資源存在時(shí)可以用MustGet,代碼更簡潔
寫在最后
這個(gè)庫的代碼量不大,但設(shè)計(jì)思路值得借鑒。泛型讓Go的資源管理終于可以做到真正的通用化,不用再為每種資源寫重復(fù)的管理代碼。
倉庫地址:github.com/qq1060656096/bizutil/registry
如果你的項(xiàng)目里也有資源管理的痛點(diǎn),不妨試試。有問題歡迎在評論區(qū)交流。
覺得有用的話,點(diǎn)個(gè)贊收藏一下,我會持續(xù)分享Go語言的實(shí)用技巧。