provider模式是go中通过函数或结构体封装依赖创建逻辑的结构约定,核心在于控制实例化时机、复用及生命周期管理,而非语言特性。

Provider 模式在 Go 里不是语言特性,而是结构约定
Go 没有内置的「Provider」抽象(不像 Spring 的 @Bean 或 Dagger 的 @Provides),所谓 Provider 模式,本质是用函数或结构体封装依赖的创建逻辑,返回具体实例。关键不在“叫什么”,而在“谁负责 new、何时初始化、如何复用”。直接写 new(MyService) 是最简 Provider;加一层 func() *MyService 就是显式 Provider 函数;包级变量 + 初始化函数则是常见变体。
容易踩的坑:
- 把 Provider 写成全局单例但没加锁,导致并发初始化竞争
- 在 Provider 函数里做耗时操作(如连接数据库)却没缓存结果,每次调用都重试
- 忘记处理依赖的关闭/清理(比如 *sql.DB 需要 Close(),但 Provider 通常不负责这个)
用函数类型 func() T 实现轻量 Provider
这是最贴近「Provider」直觉的方式:一个无参函数,返回你要的对象。它天然支持延迟初始化、可组合、易测试。
典型用法:
- 定义类型别名:
type DBProvider func() *sql.DB - 实现具体 Provider:
var ProdDBProvider DBProvider = func() *sql.DB { db, _ := sql.Open("postgres", dsn); return db } - 注入到结构体:
type UserService struct { dbProvider DBProvider },使用时调用u.dbProvider()
注意点:
- 如果返回值带 error(比如初始化可能失败),应改为 func() (*sql.DB, error),否则错误会被静默吞掉
- 不要在这个函数里做重复初始化——sql.Open 本身不建立连接,真正连接发生在第一次 Query,所以多数情况无需额外 sync.Once
结合 Wire 或 Dig 做编译期/运行时依赖图管理
手写 Provider 函数适合小项目;中大型项目建议用工具生成依赖树。Wire 和 Dig 是主流选择,但思路完全不同:
立即学习“go语言免费学习笔记(深入)”;
-
wire是编译期代码生成:你写func InitializeApp() *App和一堆ProvideXXX函数,wire gen自动生成完整初始化代码,无反射、零运行时开销 -
dig是运行时反射容器:用dig.Container.Provide()注册构造函数,靠类型签名匹配依赖,灵活但有反射成本和 panic 风险(比如类型冲突只在运行时报错)
选哪个?
- 追求确定性、怕运行时 panic → 用 wire
- 快速原型、需要动态替换(如测试时 mock)→ dig
- 不想引入新工具链 → 手写 Provider + 接口组合更可控
Provider 与接口、构造函数的边界常被混淆
Provider 不等于接口,也不等于 NewXXX 函数。三者关系是:
- 接口(如
type UserRepository interface { GetByID(id int) (*User, error) })定义契约 - 构造函数(如
func NewPostgresUserRepo(db *sql.DB) UserRepository)负责组装具体实现 - Provider(如
func() UserRepository)负责调度构造过程,可能含初始化、缓存、条件分支
常见误用:
- 把所有 NewXXX 都包装成 Provider 函数,但实际没带来任何行为差异(只是多套了一层调用)
- Provider 返回具体类型(如 *PostgresUserRepo)而非接口,破坏了依赖倒置
- 在 Provider 里硬编码配置(如 dsn 字符串),导致无法跨环境切换
复杂点往往不在 Provider 本身,而在于初始化顺序和生命周期管理——比如 HTTP server 要等 DB ready 才启动,这种协调逻辑如果全堆在 main.go 里,Provider 就只是个幌子。










