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 可能是更好的选择。这种隐式实现的程序,等到后期逻辑越来越复杂,代码篇幅越来越长,换一个不熟悉的人来维护,真有可能刚加一个功能但不知道要注册,找半天问题不知道为什么自己刚写的功能路由不到......😅

相关推荐
2601_962070232 天前
差异基因富集分析(R语言——GO&KEGG&GSEA)
开发语言·golang·r语言
「、皓子~2 天前
海狸IM 2.1 正式发布
flutter·微服务·golang·electron·开源软件·im·海狸im
运维开发笔记2 天前
8.1 Go Struct 结构体基础
golang
ttwuai2 天前
Go 后台接入 CAS 单点登录后,原来的权限怎么继续生效?
开发语言·后端·golang
蓝宝石的傻话3 天前
因为MiBeeNVR,决定接 onvif-go 自己管理并重构
数码相机·重构·golang
ttwuai3 天前
Go 后台管理系统组件库文档怎么写,才不会坑后续维护?
开发语言·javascript·golang
2601_962066233 天前
golang超详细基础入门教程_golang教程
前端·python·golang
互联网中的一颗神经元3 天前
10. Large 对象分配:直接路径
开发语言·golang
microrain4 天前
从关系库硬扛到专业时序引擎:SagooIoT时序数据存储的架构取舍
物联网·golang·开源·sagooiot