08 · 命令与订阅¶
异步工作走 Cmd 与 Subscription,不要在 View 或 Update 里散开无管理的 goroutine。
何时写完整 Program¶
ui.Run 始终接收 Program。需要下列任一能力时,使用 Program 字面量而不是 ui.NewProgram:
- 初始加载(
Init返回 Cmd) - 点击后请求网络 / 读写文件 / 延时
- 定时器、监听流(Subscription)
- 窗口状态消息
Cmd 生命周期¶
窗口销毁时,根 context.Context 会被取消。
编写 Cmd¶
DoContext(推荐阻塞/可失败工作)¶
func loadUser(id string) ui.Cmd[Msg] {
// 在 Update 里准备好不可变快照
return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
user, err := fetchUser(ctx, id) // 必须尊重 ctx
if err != nil {
return err // → OnError;或 send 一个失败消息后 return nil
}
send(UserLoaded{User: user})
return nil
})
}
Do(短、无 context)¶
捕获规则(强制)¶
| 可以 | 不可以 |
|---|---|
| 拷贝后的 string、数字、struct 值 | 保留 *Model |
| 显式拷贝的 slice/map | 保留 *ui.Context |
| 在 Update 中算好的快照 | 在 Cmd 里读会变的共享可变状态 |
// ✓
name := m.Name
return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
_ = name
return nil
})
// ✗
return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
_ = m.Name // 不要捕获 model 指针/可变引用
return nil
})
Batch¶
一次 Update 启动多个独立任务:
不要在一个 Cmd 里自己 go 一堆任务又不统一错误/取消。
LatestCmd:只要最新结果¶
搜索、预览、自动完成:
return ui.LatestCmd("search", ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
hits, err := search(ctx, query)
if err != nil {
return err
}
send(SearchResults{Hits: hits})
return nil
}))
- 同 key 的旧命令会被取消
- 旧代消息会被丢弃
- key 要稳定、数量有界(如
"search"),不要用每次请求的 UUID 当 key(key 会保留到窗口结束)
取消:
完整示例(延时加载)¶
见 examples/async:
func Update(m *Model, msg Msg) ui.Cmd[Msg] {
switch msg := msg.(type) {
case Load:
m.Loading = true
return load()
case Loaded:
m.Loading = false
m.Result = msg.Text
}
return nil
}
func load() ui.Cmd[Msg] {
return ui.DoContext(func(ctx context.Context, send ui.Send[Msg]) error {
t := time.NewTimer(time.Second)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
send(Loaded{Text: "Loaded after one second."})
return nil
}
})
}
Subscription¶
长生命周期输入:定时器、文件监听、websocket 等。
func subscriptions(m Model) []ui.Subscription[Msg] {
if !m.Ticking {
return nil
}
return []ui.Subscription[Msg]{
ui.Subscribe("clock", func(ctx context.Context, send ui.Send[Msg]) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case t := <-ticker.C:
send(Tick{At: t})
}
}
}),
}
}
规则:
| 点 | 说明 |
|---|---|
| Key | 生命周期身份;稳定则续跑,去掉则取消 |
| 空/重复 key | 编程错误 |
| 结束后 | 不会每帧自动重启;要重试请改 key 或先移除再加入 |
| 消息 | 带 generation,停掉的代不会进 Update |
嵌套模块:ui.MapSubscription(与 MapCmd 对称)。
错误处理¶
- Cmd/订阅错误、panic →
EffectError等,到OnError - 领域失败(登录失败、校验失败)→ 优先
send成消息,写进 Model - Update/View panic →
RuntimePanicError,窗口循环停止
消息队列上限 256;溢出为 QueueOverflowError。
MapCmd¶
cmd := child.Update(&m.Child, childMsg)
return ui.MapCmd(cmd, func(c ChildMsg) Msg {
return ParentWrap{c}
})
不新开生命周期,只映射消息类型。