Go用channel+goroutine替代Future/Promise,强调通信而非共享内存;channel是一次性消费的通信载体,需注意缓冲、关闭和select多路等待机制。

Go 里没有 Future 或 Promise,但有更直接的替代方案
Go 不提供 Future / Promise 这类抽象,不是遗漏,而是设计取舍:它用 channel + goroutine 组合覆盖了绝大多数异步结果获取场景。强行套用 JS/Java 风格的 then()、catch() 包装层,反而掩盖并发本质,增加调度开销和理解成本。
常见错误现象:panic: send on closed channel、deadlock、结果永远收不到——大多源于把 channel 当成“可多次读取的 Promise 对象”来用,忘了它是一次性消费的通信载体。
- 使用场景:HTTP 请求结果等待、数据库查询回调、后台任务完成通知
- channel 是同步/异步的开关:带缓冲的
make(chan T, 1)可避免发送方阻塞;无缓冲的make(chan T)要求收发双方同时就绪 - 不要重复 close 同一个 channel,也不要向已 close 的 channel 发送数据
select + chan 是最常用的“等待异步结果”写法
这不是语法糖,而是 Go 并发模型的核心机制。它天然支持超时、取消、多路等待,比手写状态机或回调链更简洁可靠。
示例:等待一个耗时操作,带 2 秒超时
立即学习“go语言免费学习笔记(深入)”;
result := make(chan string, 1)
go func() {
result <- doSomethingExpensive()
}()
<p>select {
case s := <-result:
fmt.Println("got:", s)
case <-time.After(2 * time.Second):
fmt.Println("timeout")
}-
select默认非阻塞:没有就绪 case 就走default;想阻塞等待就去掉default - 多个
case同时就绪时,select随机选一个,不保证顺序 - 别在
select外部对同一 channel 做多次接收(<-ch),channel 关闭后第二次读会得到零值,容易误判为有效结果
需要“链式调用”时,用函数返回 chan 更自然
比如你想表达 “请求 → 解析 → 校验”,不用模拟 Promise.then(),而应让每一步返回新的 chan:
func fetch(url string) chan string {
ch := make(chan string, 1)
go func() { ch <- httpGet(url) }()
return ch
}
<p>func parse(ch chan string) chan int {
out := make(chan int, 1)
go func() { out <- len(<-ch) }()
return out
}</p><p>// 使用:
length := <-parse(fetch("<a href="https://www.php.cn/link/b05edd78c294dcf6d960190bf5bde635">https://www.php.cn/link/b05edd78c294dcf6d960190bf5bde635</a>"))- 每个函数只关心输入 channel 和输出 channel,职责清晰,易于测试
- 注意缓冲大小:若中间环节处理慢,上游 goroutine 可能被阻塞(尤其无缓冲 channel)
- 无法像
Promise.catch()那样集中捕获错误——错误需随结果一并传入 channel,例如用chan struct{ val T; err error }
第三方库如 gofuture 或 promise 包往往得不偿失
它们确实提供了 Then()、Catch() 方法,但底层仍是 channel 封装。问题在于:
- 隐藏了 goroutine 生命周期管理:谁启动?谁回收?出错是否泄漏?
- 错误传播路径变长,调试时 stack trace 难以定位到真正出错的 goroutine
- 和标准库生态不融合:比如
context.Context取消信号无法自然注入到Then()链中 - 性能损耗虽小,但在高频调用场景(如微服务内部 pipeline)会累积
真有复杂依赖编排需求,优先考虑 errgroup.Group + context.WithTimeout,而不是引入 Promise 抽象。
最容易被忽略的一点:channel 的零值是 nil,对 nil channel 的操作会永久阻塞(select 中该 case 永远不会触发)。初始化、传递、检查 channel 时,别让它意外变成 nil。










