G

[Golang] Map注册表优化多个if-else

RoLingG Golang 2026-01-05

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)
    }
}

这样优化能够有下面优势:

  1. 时间复杂度从 O(n)→O(1)

    原来每新增一种格式,函数里就多一个 else if,判断次数随格式线性增长;map 是哈希表,一次定位,与格式数量无关。

  2. 与配置/插件系统天然对接
    未来如果格式列表放到 JSON/YAML 配置,甚至由外部插件动态注册,只要 exporters[name] = plugin.Export 即可,核心调用代码一行不动。
  3. 与中间件/装饰器模式无缝结合
    例如需要给所有导出加统一日志、度量、权限检查,只要写一层高阶函数:

    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,
        }
    })
}

大概最终优化就是这样,结束力。

PREV
[MongoDB] MongoDB-CVE-2025-14847漏洞
NEXT
[Golang] 从字节Netpoll中学习相关Read问题处理

评论(0)

发布评论