Map注册表优化多个if-else
我们通过一个简单的数据转成各项格式的demo进行优化,我们先来看优化后的代码:
// 优化后
package main
import (
"errors"
"fmt"
)
// ExportFn 是导出函数的类型
type ExportFn func(data interface{})
// exporters 映射格式到对应的导出函数
var exporters = map[string]ExportFn{
"pdf": exportPDF,
"csv": exportCSV,
"json": exportJSON,
}
// exportData 根据格式导出数据
func exportData(data interface{}, format string) error {
exporter, exists := exporters[format] // 不同格式获得对应格式的闭包调用
if !exists {
return errors.New("no exporter found for format: " + format)
}
exporter(data) // 闭包调用传输数据获得想要结果
return nil
}
// exportPDF 导出为PDF格式
func exportPDF(data interface{}) {
fmt.Println("Exporting data as PDF...")
}
// exportCSV 导出为CSV格式
func exportCSV(data interface{}) {
fmt.Println("Exporting data as CSV...")
}
// exportJSON 导出为JSON格式
func exportJSON(data interface{}) {
fmt.Println("Exporting data as JSON...")
}
func main() {
data := map[string]string{"key": "value"}
err := exportData(data, "pdf")
if err != nil {
fmt.Println(err)
}
err = exportData(data, "csv")
if err != nil {
fmt.Println(err)
}
err = exportData(data, "json")
if err != nil {
fmt.Println(err)
}
}如果不用的话,就是这样的:
// 优化前
package main
import (
"errors"
"fmt"
)
// exportData 根据格式导出数据
func exportData(data interface{}, format string) error {
if format == "pdf" {
exportPDF(data)
} else if format == "csv" {
exportCSV(data)
} else if format == "json" {
exportJSON(data)
} else {
return errors.New("no exporter found for format: " + format)
}
return nil
}
// exportPDF 导出为PDF格式
func exportPDF(data interface{}) {
fmt.Println("Exporting data as PDF...")
}
// exportCSV 导出为CSV格式
func exportCSV(data interface{}) {
fmt.Println("Exporting data as CSV...")
}
// exportJSON 导出为JSON格式
func exportJSON(data interface{}) {
fmt.Println("Exporting data as JSON...")
}
func main() {
data := map[string]string{"key": "value"}
err := exportData(data, "pdf")
if err != nil {
fmt.Println(err)
}
err = exportData(data, "csv")
if err != nil {
fmt.Println(err)
}
err = exportData(data, "json")
if err != nil {
fmt.Println(err)
}
}这样优化能够有下面优势:
时间复杂度从 O(n)→O(1)
原来每新增一种格式,函数里就多一个
else if,判断次数随格式线性增长;map 是哈希表,一次定位,与格式数量无关。- 与配置/插件系统天然对接
未来如果格式列表放到JSON/YAML配置,甚至由外部插件动态注册,只要exporters[name] = plugin.Export即可,核心调用代码一行不动。 与中间件/装饰器模式无缝结合
例如需要给所有导出加统一日志、度量、权限检查,只要写一层高阶函数:func logged(fn ExportFn) ExportFn { return func(data interface{}) { log.Println("start export") fn(data) log.Println("end export") } } exporters["pdf"] = logged(exportPDF)对
exportData依旧零感知。
但这都是基于多个 if-else 的场景下,格式只有 1~2 种,且业务层明确“永远不会再加”——直接 if 最直观。而且性能瓶颈在导出算法本身,路由耗时占比可以忽略——优化收益微乎其微。
后日谈-再优化
我们可以看到上面注册表:
// exporters 映射格式到对应的导出函数
var exporters = map[string]ExportFn{
"pdf": exportPDF,
"csv": exportCSV,
"json": exportJSON,
}假设我们又有将近 20 个需要映射的导出函数,这样注册表岂不是要有 20+ 行?这会显得代码繁杂,降低易读性,这显然与我们一开始需要优化这个 if-else 的理念有些冲突了。
那么既然一个文件内代码看起来繁杂,那我们拆开就好了,专项专职。
使用自动注册模式(插件式设计),不再通过一个大 map 静态声明,而是让每个导出模块在初始化时“自我介绍”。这利用了 Go 语言的 init() 函数特性。
/pkg/export/
├── main.go // 核心逻辑函数
├── pdf.go // PDF 具体实现 (含 init 注册)
└── csv.go // csv 具体实现 (含 init 注册)// main.go
package main
import (
"errors"
"fmt"
)
// ExportFn 是导出函数的类型
type ExportFn func(data interface{})
// Register 暴露一个公开方法供其他包注册自己
func Register(format string, exporter ExportFn) {
exporters[format] = exporter
}
// exporters 映射格式到对应的导出函数
var exporters = make(map[string]ExportFn)
// exportData 根据格式导出数据
func exportData(data interface{}, format string) error {
exporter, exists := exporters[format] // 不同格式获得对应格式的闭包调用
if !exists {
return errors.New("no exporter found for format: " + format)
}
exporter(data) // 闭包调用传输数据获得想要结果
return nil
}
func main() {
data := map[string]string{"key": "value"}
err := exportData(data, "pdf")
if err != nil {
fmt.Println(err)
}
err = exportData(data, "csv")
if err != nil {
fmt.Println(err)
}
err = exportData(data, "json") // error test
if err != nil {
fmt.Println(err)
}
}// csv.go
package main
import "fmt"
// init 注册函数会在程序编译时就自动运行
// 能达到自动初始化 Register map 的效果
func init() {
Register("csv", exportInitCSV)
}
// exportPDF 导出为CSV格式
func exportInitCSV(data interface{}) {
fmt.Println("Exporting data as CSV...")
}// pdf.go
package main
import (
"fmt"
)
// init 注册函数会在程序编译时就自动运行
// 能达到自动初始化 Register Map 的效果
func init() {
Register("pdf", exportInitPDF)
}
// exportPDF 导出为PDF格式
func exportInitPDF(data interface{}) {
fmt.Println("Exporting data as PDF...")
}这里优化用了公开注册的方式:
// Register 暴露一个公开方法供其他包注册自己
func Register(format string, exporter ExportFn) {
exporters[format] = exporter
}配合各个导出函数的 init 注册函数,能够很好的区分开函数模块职责与功能,优化代码繁杂与可读性问题。
优点是增加新格式只需要新建文件,无需修改主逻辑代码,符合开闭原则。
优化の优化
上一个优化虽然解决了代码繁杂带来的可读性问题,但核心上还是通过不同模块的 init 函数将需要注册的功能往 exporter 这个 map 里面塞。
// exporters 映射格式到对应的导出函数
var exporters = make(map[string]ExportFn)这会有个隐性问题,如果每个导出逻辑非常复杂(例如 PDF 导出需要加载庞大的字体库或第三方 SDK),直接在 map 里存函数会导致程序启动时占用过多内存。
要解决这个问题,我们只能放弃 map 中存储函数实例,转为存储构造器(Factory)。
也就是用从存储 “处理函数”,转为存储 “具有处理函数的对象生成函数”。通过调用生成函数获得对象,再调用对象的处理函数,这样就解决了。
// main.go
package main
import (
"fmt"
)
func main() {
data := map[string]string{"key": "value"}
err := ExportData(data, "pdf")
if err != nil {
fmt.Println(err)
}
err = ExportData(data, "csv")
if err != nil {
fmt.Println(err)
}
err = ExportData(data, "json")
if err != nil {
fmt.Println(err)
}
}// exporter.go
package main
import "fmt"
// Exporter 定义导出器必须实现的方法
type Exporter interface {
Export(data interface{}) error
}
// 注册表,存储的是“如何创建对象”的函数(工厂函数),而不是处理函数本身
var registry = make(map[string]func() Exporter)
// Register 提供给各个子模块调用的注册函数
func Register(format string, factoryFunc func() Exporter) {
registry[format] = factoryFunc
}
// ExportData 统一入口
func ExportData(data interface{}, format string) error {
factory, ok := registry[format]
if !ok {
return fmt.Errorf("unsupported format: %s", format)
}
// 运行到这里才真正通过工厂函数创建对象
instance := factory()
return instance.Export(data)
}// pdf-factory
package main
import "fmt"
// pdfExporter 是私有的,外部无法直接创建,只能通过注册表获取
type pdfExporter struct {
// 可以在这里添加 PDF 专用的配置,比如字体、颜色等
fontConfig string
}
func (p *pdfExporter) Export(data interface{}) error {
fmt.Printf("PDF Exporting with config [%s]: %v\n", p.fontConfig, data)
return nil
}
// 注册逻辑
func init() {
Register("pdf", func() Exporter {
// 这里可以进行复杂的初始化逻辑
return &pdfExporter{fontConfig: "Arial"}
})
}// csv-factory
package main
import "fmt"
type csvExporter struct {
config string
}
func (csv *csvExporter) Export(data interface{}) error {
fmt.Printf("CSV Exporting with config [%s]: %v\n", csv.config, data)
return nil
}
func init() {
Register("csv", func() Exporter {
return &csvExporter{config: "utf-8"}
})
}而且因为传的函数获得的是对象,从而可以在对象里声明一些设置,让处理函数更灵活,变量声明更可读。
package main
import (
"fmt"
"time"
)
type csvExporter struct {
config string
delimiter string // 分隔符,比如逗号或分号
createdAt time.Time // 记录导出器创建时间
logger func(string) // 模拟一个内部使用的日志函数
}
func (csv *csvExporter) Export(data interface{}) error {
csv.logger(fmt.Sprintf("开始导出,配置为: %s", csv.config))
fmt.Printf("[%s] CSV Exporting (Delimiter: '%s') with config [%s]: %v\n",
csv.createdAt.Format("15:04:05"), csv.delimiter, csv.config, data)
return nil
}
func init() {
// 这里的闭包就是所谓的“工厂”
Register("csv", func() Exporter {
// 1. 模拟复杂的逻辑判断
currHour := time.Now().Hour()
var bestDelimiter string
if currHour < 12 {
bestDelimiter = ","
} else {
bestDelimiter = ";"
}
// 2. 模拟依赖注入(注入一个日志函数)
internalLogger := func(msg string) {
fmt.Printf("[INTERNAL LOG]: %s\n", msg)
}
// 3. 模拟耗时操作(如预加载配置)
// time.Sleep(time.Millisecond * 10)
// 最后返回组装好的结构体指针
return &csvExporter{
config: "UTF-8-BOM",
delimiter: bestDelimiter,
createdAt: time.Now(),
logger: internalLogger,
}
})
}大概最终优化就是这样,结束力。
RoLingG | 博客
评论(0)