90%的Go都在重复造轮子:一个泛型资源管理库帮你终结连接池噩梦

90%的Go开发者都在重复造轮子:一个泛型资源管理库帮你终结连接池噩梦

你有没有遇到过这种情况?

项目里同时用了MySQL、Redis、MongoDB,每个都要写一套初始化逻辑。配置散落在各个角落,关闭资源的时候漏掉一个,内存泄漏查半天。更崩溃的是,换个项目又得把这些代码复制粘贴一遍。

今天介绍一个开源库,用不到300行代码,彻底解决资源管理的混乱问题。

image.png

一、核心设计:三个概念搞定一切

这个库的设计极其精简,只有三个核心概念:

Opener —— 告诉库怎么创建资源
Closer —— 告诉库怎么销毁资源
Manager —— 帮你统一管理所有资源

看一眼类型定义你就懂了:

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泛型的威力在这里体现得淋漓尽致——一套代码管理所有类型的资源

二、惰性初始化:不用不创建

很多人写资源管理,喜欢在程序启动时把所有连接都建好。数据库连接池、Redis客户端、各种SDK,一股脑全初始化。

问题来了:如果某个服务这次请求根本用不到MongoDB,为什么要提前建连接?

这个库采用惰性初始化策略。你先注册配置,资源在第一次Get的时候才真正创建

// 注册只保存配置,不创建连接
group.Register(ctx, "main-db", dbConfig)
group.Register(ctx, "cache", redisConfig)

// 首次调用才真正建立连接
db, err := group.Get(ctx, "main-db")

这个设计带来两个好处:启动速度快,资源按需分配。

三、并发安全:双重检查锁的教科书实现

多个goroutine同时请求同一个资源怎么办?

库里用了经典的双重检查锁定模式。先用读锁快速判断资源是否就绪,没就绪再升级写锁创建。这样既保证了并发安全,又避免了每次访问都加重锁的性能损耗。

// 读锁:快速路径
g.m.mu.RLock()
if conn.ready {
    val := conn.val
    g.m.mu.RUnlock()
    return val, nil
}
g.m.mu.RUnlock()

// 写锁:慢速路径,惰性创建
g.m.mu.Lock()
defer g.m.mu.Unlock()

// 二次检查,防止重复创建
if conn.ready {
    return conn.val, nil
}

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

这段代码可以当作Go并发编程的范本来学习。

四、分组管理:多租户场景的救星

实际项目中,你可能需要管理多套同类型的资源。比如SaaS系统里,每个租户一套数据库配置。

这个库原生支持分组:

manager := registry.New(dbOpener, dbCloser)

// 按租户分组
manager.AddGroup("tenant-A")
manager.AddGroup("tenant-B")

// 获取租户A的数据库
groupA, _ := manager.Group("tenant-A")
groupA.Register(ctx, "primary", tenantAConfig)
db, _ := groupA.Get(ctx, "primary")

每个Group独立管理自己的资源,互不干扰。关闭时可以单独关闭某个组,也可以一键关闭整个Manager。

五、优雅关闭:告别资源泄漏

程序退出时忘记关闭连接,这种bug隐蔽又致命。

库提供了统一的Close方法,自动遍历所有已创建的资源并关闭:

// 关闭单个组
errs := group.Close(ctx)

// 关闭整个管理器
errs := manager.Close(ctx)

返回值是错误切片,方便你知道哪些资源关闭失败了。

六、实战:GORM接入完整示例

废话不多说,直接上可运行的代码:

package main

import (
    "context"
    "fmt"

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

// 数据库配置
type DBConfig struct {
    DSN string
}

// 创建数据库连接(opener)
func openDB(ctx context.Context, cfg DBConfig) (*gorm.DB, error) {
    return gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{})
}

// 关闭数据库连接(closer)
func closeDB(ctx context.Context, db *gorm.DB) error {
    sqlDB, _ := db.DB()
    return sqlDB.Close()
}

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

    // 创建 registry(单组模式)
    DB := registry.NewGroup[DBConfig, *gorm.DB](
        openDB,
        closeDB,
    )

    // 注册数据库(此时不会真正连接)
    DB.Register(ctx, "main", DBConfig{
        DSN: "user:pass@tcp(127.0.0.1:3306)/test?parseTime=true",
    })

    // 第一次获取时才创建连接
    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]

    // 程序退出时统一关闭
    DB.Close(ctx)
}

整个流程清晰明了:定义opener和closer → 创建registry → 注册配置 → 按需获取 → 统一关闭。

如果你的项目需要管理多套数据库(比如读写分离、多租户),用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")

// 程序退出时一键关闭所有连接
defer manager.Close(ctx)

七、源码亮点

翻了一遍源码,有几个细节值得学习:

  1. 泛型约束用any:最大化灵活性,任何类型都能管理
  2. Closer可以为nil:有些资源不需要显式关闭,设计上考虑到了
  3. 错误类型丰富:ErrGroupNotFound、ErrResourceNotFound、ErrCloseResourceFailed,定位问题很方便
  4. Must系列方法:确定资源存在时可以用MustGet,代码更简洁

写在最后

这个库的代码量不大,但设计思路值得借鉴。泛型让Go的资源管理终于可以做到真正的通用化,不用再为每种资源写重复的管理代码。

仓库地址:github.com/qq1060656096/bizutil/registry

如果你的项目里也有资源管理的痛点,不妨试试。有问题欢迎在评论区交流。


觉得有用的话,点个赞收藏一下,我会持续分享Go语言的实用技巧。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容