Go - 注册表工厂模式的简单实现

前言

昨天在整合之前写的那些定时运行的小命令行工具,比如检测 DB 证书有效期、更新 Nginx SSL 证书、数据备份等。早期需求少,这些十几个小工具都是后期逐渐加上去的,一个工具就占一个文件夹,找起来还挺麻烦的,索性就整合成一个程序,根据命令行传参 job 名来运行指定 job。AI review 的时候说这是"基于 init 自注册的注册表工厂模式",是个很常见的惯用写法。有点拗口,我缩减了一下就干脆叫"注册表工厂模式"。

PS: 之前写 Python 的时候经常用 __init_subclass__() 实现继承即注册,再按类名调用对应的功能。Go 使用 init() 方法也能实现,算是异曲同工吧。

模式介绍

这种模式最明显的特征就是在 init() 函数中将工厂方法注册到一个"注册表"中,从注册表中按名称或类型来调用工厂方法做实例化, 调用注册表方法的地方必定会有类似这样的 import: _ "xxx"

最大的好处就是出问题 debug 的时候让同事找半天 灵活 ,新增功能只要在新代码中用 init() 注册,不需要手动维护注册表。核心逻辑不需要知道有哪些实现,第三方包通过被导入触发 init() 自注册来扩展能力。

比如标准库 database/sql 就是这么做的, sql.Register 注册驱动,sql.Open("sqlite", dsn) 按名字取。

示例代码

实际代码篇幅有点长,我就另外写了个简化版方便理解。

代码目录结构

复制代码
├── go.mod
├── internal
│   ├── jobs  # 实际 job
│   │   ├── joba.go
│   │   └── jobb.go
│   └── register
│       └── register.go  # 注册表
├── main.go
├── Makefile
└── std-go-learn.bin  # 编译后的文件

注册表

注册表模块中声明 job 接口,以及注册表方法 Register()Create()List() 方法只是个辅助方法,让用户体验稍微好点,知道有哪些已注册的 job。

在定义注册表的模块中,不要导入有具体任务的模块,避免循环依赖。

go 复制代码
// internal/register/register.go
package register

import "fmt"

type Job interface {
	Name() string
	Run() error
}

// 存储已注册的 Job 类型
var factories = make(map[string]func() Job)

// Register 注册一个 Job 类型
func Register(name string, factory func() Job) {
	// 检查是否已注册, 防止同名注册
	if _, ok := factories[name]; ok {
		panic(fmt.Sprintf("job %s already registered", name))
	}
	factories[name] = factory
}

// Create 创建一个 Job 实例
func Create(name string) (Job, error) {
	if f, ok := factories[name]; ok {
		return f(), nil
	}
	return nil, fmt.Errorf("job not found: %s", name)
}

func List() string {
	availableJobs := ""
	for name := range factories {
		availableJobs += "- " + name + "\n"
	}
	return availableJobs
}

具体功能的结构体

作为示例,功能很简单,核心是实现接口和在 init() 中注册。

go 复制代码
// internal/jobs/joba.go
package jobs

import (
	"fmt"
	"std-go-learn/internal/register"
)

func init() {
	const jobName = "jobA"
	register.Register(jobName, func() register.Job {
		return &JobA{name: jobName}
	})
}

type JobA struct {
	name string
}

func (j *JobA) Name() string {
	return j.name
}

func (j *JobA) Run() error {
	fmt.Println("JobA is running")
	return nil
}


// internal/jobs/jobb.go
package jobs

import (
	"fmt"
	"std-go-learn/internal/register"
)

func init() {
	const jobName = "jobB"
	register.Register(jobName, func() register.Job {
		return &JobB{name: jobName}
	})
}

type JobB struct {
	name string
}

func (j *JobB) Name() string {
	return j.name
}

func (j *JobB) Run() error {
	fmt.Println("JobB is running")
	return nil
}

程序入口

在调用注册表的地方导入具体任务模块,做一些简单处理。

go 复制代码
package main

import (
	"flag"
	"fmt"
	"os"
	_ "std-go-learn/internal/jobs"
	"std-go-learn/internal/register"
)

func main() {
	// flag.StringVar(&flagJobName, "j", "", "job name")
	jobName := flag.String("j", "", "job name")
	flag.Parse()

	if *jobName == "" {
		fmt.Println("Must specify job name")
		flag.Usage()
		os.Exit(1)
	}

	job, err := register.Create(*jobName)
	if err != nil {
		fmt.Println("Failed to create job:", err)
		fmt.Printf("Available jobs:\n%s", register.List())
		os.Exit(1)
	}
	fmt.Println("Job name:", job.Name())
	if err := job.Run(); err != nil {
		fmt.Println("Failed to run job:", err)
		os.Exit(1)
	}
	fmt.Println("Job run successfully")
}

运行测试

正常调用 2 个已注册的 job

shell 复制代码
$ ./std-go-learn.bin -j jobA
Job name: jobA
JobA is running
Job run successfully

$ ./std-go-learn.bin -j jobB
Job name: jobB
JobB is running
Job run successfully

调用不存在的 job 会报错,并且退出码为非0,方便 shell 脚本判断。

shell 复制代码
$ ./std-go-learn.bin -j jobC
Failed to create job: job not found: jobC
Available jobs:
- jobA
- jobB
  
$ echo $?
1

补充

如果功能很少,也没后期扩展的需求,用 if ... else 或者 switch ... case 可能是更好的选择。这种隐式实现的程序,等到后期逻辑越来越复杂,代码篇幅越来越长,换一个不熟悉的人来维护,真有可能刚加一个功能但不知道要注册,找半天问题不知道为什么自己刚写的功能路由不到......😅

相关推荐
圣殿骑士-Khtangc13 小时前
Go-Map面试核心考点从哈希冲突到并发安全的完整解析
面试·golang·哈希算法
worxfr13 小时前
Go 并发控制:从 Channel 方向约束到实战模式
开发语言·后端·golang
xcLeigh14 小时前
Go入门:短变量声明的陷阱与最佳实践
java·redis·golang·教程·变量
JavaPub-rodert14 小时前
我又把自己的 Go 后台管理系统升级了一遍:文件管理、2GB 上传、私有文件预览、Docker 镜像全安排上了
开发语言·docker·golang·shiyuadmin
名字还没想好☜15 小时前
Go 的 sync.Cond 实战:用条件变量做等待/通知,比忙轮询省 CPU
开发语言·数据库·后端·golang·go
圣殿骑士-Khtangc17 小时前
Go-Select多路复用机制的面试真题与底层实现
开发语言·面试·golang
啊哈一半醒2 天前
Go 语言 Context 全方位详解:原理、实战与避坑
开发语言·后端·golang
名字还没想好☜2 天前
Go 的 io.Reader/Writer 组合实战:io.Copy、TeeReader、MultiWriter 优雅处理数据流
开发语言·后端·golang·go·iphone