go中factory扩展的核心是配置驱动+接口契约+运行时注册,而非硬编码map[string]func() interface;因后者导致新增类型需改代码、重编译、无法被第三方模块动态注册,且存在并发安全与初始化时机问题。

Go 里的 factory 扩展,核心不是“加新类型时改一堆代码”,而是让新增实现不碰原有注册表、不改工厂函数、不重编译主逻辑——靠配置驱动 + 接口契约 + 运行时注册达成。
为什么硬编码 map[string]func() Interface 会卡住扩展
常见写法是在 init() 里填满 creators["mysql"] = func() Database { return &MySQLDB{} },看似干净,但问题很实际:
- 所有支持的类型必须在编译期写死,加个
"tidb"就得改代码、重新构建 - 第三方模块(比如你写的
github.com/your-org/pgx-adapter)没法往主工厂里注册自己的实现 - 配置项(如
driver: "mysql")和构造函数强绑定,换驱动就得同步改 YAML 和 Go 注册逻辑 -
map默认非并发安全,若在init()外动态注册(比如插件加载),可能 panic
用 Register(name, ctor) 替代 init() 静态注册
把注册行为从“启动时自动执行”变成“可被外部调用的函数”,是解耦第一步。关键点:
- 注册函数本身要线程安全:用
sync.Once初始化内部map,读多写少场景用sync.RWMutex包裹写操作 - 注册时机由使用者控制:main 包按配置决定调用
Register("redis", newRedisCache)还是Register("memcached", newMemcacheCache) - 构造函数签名统一为
func(config Config) (Interface, error),把配置解析提前到注册前,工厂函数只负责调用 - 避免在注册时执行构造函数:
creators[name] = func() Interface { return &RedisCache{} }是错的;正确是存函数值,延迟到NewXXX(name)时才调用
示例节选:
var (
creators = make(map[string]func(Config) (Cache, error))
once sync.Once
mu sync.RWMutex
)
func Register(name string, ctor func(Config) (Cache, error)) {
once.Do(func() {
creators = make(map[string]func(Config) (Cache, error))
})
mu.Lock()
defer mu.Unlock()
creators[name] = ctor
}
func NewCache(name string, cfg Config) (Cache, error) {
mu.RLock()
ctor, ok := creators[name]
mu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown cache type: %s", name)
}
return ctor(cfg)
}
配置如何驱动具体实现选择
配置不能只传字符串名,得带上下文。否则 name == "mysql" 时,工厂不知道连哪个地址、用什么用户名——那初始化逻辑又得塞回工厂里,违背“轻量”原则。
- 配置结构体应由具体实现定义,工厂只接收并透传:比如
mysql.Config和postgres.Config是两个独立结构体,都实现cache.Config接口(若需统一抽象)或直接作为参数传入 - 推荐做法:配置解析与工厂分离。先用 viper 或 json 解出原始 map,再根据
type字段分发给对应解析器,生成具体Config实例,最后喂给NewXXX(name, config) - 不要让工厂承担配置校验:字段缺失、端口非法等应在
mysql.NewConfig()内部检查并返回 error,工厂只管调用和透传 - 环境变量或 flag 可覆盖配置文件中
type字段,实现 dev/staging/prod 下切换实现而不改 YAML
插件化扩展时最容易漏掉的三件事
当你要支持用户自己写个 my-oss-storage 并注入工厂时,以下三点常被忽略:
- 注册必须发生在
main()开始之后、首次调用工厂之前——否则sync.Once已触发,后续Register()无效 - 插件包需显式调用
init()或暴露一个Setup()函数,否则 Go 不会加载其注册逻辑(包未被引用时不会执行init) - 错误处理不能只靠 panic 或 log.Fatal:工厂函数返回
error,上层应能捕获并降级(比如 fallback 到内存缓存),而不是整个服务启动失败
真正难的从来不是写一个能工作的工厂,而是让第 5 个团队成员在不看文档、不问老同事的情况下,也能安全地加一种新数据库驱动——这要求注册路径清晰、错误信息具体、配置字段可发现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











