G

[Golang] 使用泛型和方法表达式简化消息分发

RoLingG Golang 2026-08-08

使用泛型和方法表达式简化消息分发

最近在重构优化 Bubble Tea 框架的终端工具项目时(类Elm 架构),发现顶层 Update 里有两段负责消息分发的 switch,代码几乎一样,就是模块名不同。每加一个模块,就要多抄两份。这篇文章主要记录一下这次用泛型和方法表达式收敛重复代码的过程。

重复的样板代码

这个项目是一个 8 模块的终端应用,架构是典型的 Bubble Tea 写法:一个顶层 model 持有各个子模块的指针 m.gitm.logm.weather……事件被框架封装成 tea.Msg,流进顶层 model.Update,由它决定这条消息交给哪个模块。

这个 "交给谁" 的逻辑分两部分。异步加载的结果(比如 log.LoadMsg)可能在用户切到别的 Tab 之后才返回,必须按消息类型路由给对应的模块,否则当前激活的模块会把它丢掉;剩下其余消息,则交给当前激活的 Tab。

于是就有了下面这种代码:

// 跨模块消息:异步加载结果可能在用户切到其他 Tab 后才返回
// 必须在顶层拦截,否则当前 Tab 的模块会丢弃这些消息
crossHandled := false
switch msg := msg.(type) {
case log.LoadMsg, log.DirMsg, log.TailDataMsg:
    var cmd tea.Cmd
    m.log, cmd = m.log.Update(msg)
    if cmd != nil {
        cmds = append(cmds, cmd)
    }
    crossHandled = true
case git.InfoMsg, git.DirMsg:
    var cmd tea.Cmd
    m.git, cmd = m.git.Update(msg)
    if cmd != nil {
        cmds = append(cmds, cmd)
    }
    crossHandled = true
// ... config / system / ports / net / route,5 组几乎一样的代码
}

if !crossHandled {
    switch m.state {
    case ui.TabGit:
        var cmd tea.Cmd
        m.git, cmd = m.git.Update(msg)
        cmds = append(cmds, cmd)
    // ... 剩下 7 个 Tab 同样
    }
}

return m, tea.Batch(cmds...)

两个 switch 加起来 15 个 case,每个 case 都是同样六七行:调用模块的 Update、取回命令、判空追加。

    var cmd tea.Cmd
    m.xxx, cmd = m.xxx.Update(msg)
    if cmd != nil {
        cmds = append(cmds, cmd)
    }
    crossHandled = true

去掉模块名之后,每一段做的事其实一模一样——「把这个模块的 Update 跑一遍,命令收集起来」。

机会来自签名完全相同

重构的契机是发现了一个事实:8 个模块的 Update 签名完全一致。

git.Model      func(m *Model, msg tea.Msg) (*Model, tea.Cmd)
log.Model      func(m *Model, msg tea.Msg) (*Model, tea.Cmd)
weather.Model  func(m *Model, msg tea.Msg) (*Model, tea.Cmd)
... 其余 5 个  完全相同

如果它们签名不一样,这套方案就做不了;正因为一致,才能用一个泛型参数 T 把它们统一成一个模板。

用泛型和方法表达式收敛

先加一个泛型函数,把 "调 Update、收集命令、返回新模块" 这套动作装进去:

// updateForward 执行子模块 Update,把返回的非 nil cmd 追加进 cmds,返回更新后的模块。
func updateForward[T any](mode T, msg tea.Msg, cmds []tea.Cmd,
    updateFunc func(T, tea.Msg) (T, tea.Cmd)) (T, []tea.Cmd) {
    newMode, cmd := updateFunc(mode, msg)
    if cmd != nil {
        cmds = append(cmds, cmd)
    }
    return newMode, cmds
}

然后原来的两个 switch 就变成了:

// 跨模块拦截:每个 case 3 行
crossHandled := false
switch msg := msg.(type) {
case log.LoadMsg, log.DirMsg, log.TailDataMsg:
    crossHandled = true
    m.log, cmds = updateForward(m.log, msg, cmds, (*log.Model).Update)
case git.InfoMsg, git.DirMsg:
    crossHandled = true
    m.git, cmds = updateForward(m.git, msg, cmds, (*git.Model).Update)
// ... 其余 5 组同样
}

// 当前 Tab 分发:每个 case 1 行
if !crossHandled {
    switch m.state {
    case ui.TabGit:
        m.git, cmds = updateForward(m.git, msg, cmds, (*git.Model).Update)
    case ui.TabLog:
        m.log, cmds = updateForward(m.log, msg, cmds, (*log.Model).Update)
    // ... 其余 6 个 Tab 同样
    }
}

return m, tea.Batch(cmds...)

代码量从 97 行压到 53 行,每个 case 只留下 "哪个模块、哪个 Update" 这两个信息。巧妙的利用好方法表达式和泛型约束,使模块内部零改动。

总结

这次重构里最值得的不是代码少了多少行,而是很好的实践了泛型和方法表达式优化代码。当然,这套优化模式也不是到处都适用。前提是多个类型的同名方法签名完全一致,调用方又需要统一转发。如果只是两三个地方重复,强行泛型反而引入概念负担,那不如直接类复制写下去。

PREV
[Golang] 一次简单的 BubbleTea 消息循环分析
NEXT
[Golang] Golang 1.26版本新GC回收——Green Tea 🍵 Garbage Collector(补完)

评论(0)

发布评论