写 Rust 异步代码的人,大概都经历过这么一个阶段:刚开始用 tokio::select!,觉得它真香,等项目一跑起来,分支越来越多,你发现这段代码越来越不想碰。

不是因为它逻辑复杂,而是它长得越来越难看。


1. 格式化不了的宏,就像没洗的碗

你有没有过这种体验:写完一段 tokio::select!,按下保存键,期待编辑器帮你把缩进对齐、把行宽理顺。结果呢?纹丝不动。

这不是你编辑器的问题,是 rustfmt 的问题。

tokio::select! 内部用了一套自定义 DSL(领域特定语言),rustfmt 根本不认识它。对格式化工具来说,这个宏就是一团不可解析的字符。它只能跳过,假装没看见。

这就好比你厨房里放了一堆没洗的碗。碗本身没问题,功能完好,但你就是不想再碰它。因为每次看到都觉得别扭,但又懒得动手。


2. better_tokio_select 来了

2026 年 3 月底,一个叫 better_tokio_select 的 crate 出现在 Rust 社区论坛上。作者是 nik-rev,crate 的目标非常聚焦:让 tokio::select! 能被 rustfmt 格式化。

它导出的宏叫 tokio_select!,功能和 tokio::select! 一模一样,支持条件分支、biased 选择、else 回退,一个都不少。

区别在语法上。

tokio::select! 的写法是:

tokio::select! {
    Ok(res) = reader.read(&mut buf), if can_read => {
        writer.write_all(res.bytes).await?;
    }

    _ = shutdown.recv() => {
        return Ok(());
    }
}

tokio_select! 的写法是:

tokio_select!(match .. {
    .. if let Ok(res) = reader.read(&mut buf) && can_read => {
        writer.write_all(res.bytes).await?;
    }

    .. if let _ = shutdown.recv() => {
        return Ok(());
    }
})

看到那些 .. 了吗?它们看起来确实有点怪。但正是这些省略号模式,让整个表达式变成了合法的 Rust 语法,rustfmt 终于能认出它、格式化它了。


3. 为什么 rustfmt 认不出 tokio::select!

说白了,原因不复杂。

rustfmt 的工作原理是解析 Rust 语法树,然后按规则重新排版。它能处理函数体、match 语句、if 表达式这些"标准"语法。但宏不一样。

rustfmt 遇到一个宏调用,它会检查宏的参数是不是合法的 Rust 表达式。如果是,它就能格式化。如果不是,它就跳过。

tokio::select! 的参数是一个自定义语法块,里面用了 <pattern> = <expr>, if <cond> => <handler> 这种写法。这种语法在 Rust 里没有对应的表达式形式,所以 rustfmt 无能为力。

better_tokio_select 的聪明之处在于:它把分支写成了 match 表达式的 arm,而 matchrustfmt 完全支持的标准语法。那些 .. 是省略号模式(.. pattern),在 Rust 里是合法的。

这就像你想让快递员把包裹放在门口,但你家门牌号写的是火星文。快递员看不懂,只能原路退回。现在你换了个正常门牌号,包裹就到了。


4. 实际代码长什么样

来看几个真实场景的对比。

TCP 代理:读写 + 优雅关闭

这是网络编程里最常见的模式:一边读数据,一边监听关闭信号。

tokio::select!

tokio::select! {
    res = reader.read(&mut buf), if can_read => {
        let n = res?;
        if n == 0 { return Ok(()); }
        writer.write_all(&buf[..n]).await?;
    }

    _ = shutdown.recv() => {
        return Ok(());
    }
}

tokio_select!

tokio_select!(match .. {
    .. if let Ok(n) = reader.read(&mut buf) && can_read => {
        let n = res?;
        if n == 0 { return Ok(()); }
        writer.write_all(&buf[..n]).await?;
    }

    .. if let _ = shutdown.recv() => {
        return Ok(());
    }
})

逻辑一模一样,写法略有不同。但后者能被 rustfmt 一键格式化。

消息处理器:带优先级的选择

在消息队列场景里,你可能想优先处理某些类型的消息。biased 关键字能让 select 从上到下按顺序检查分支,而不是随机轮询。

tokio_select!(biased, match .. {
    .. if let Some(Message::Data { id, payload }) = rx.recv() => {
        process(id, payload).await;
    }

    .. if let Some(Message::Control(cmd)) = rx.recv() => {
        handle_command(cmd).await;
    }

    _ => {
        println!("no messages pending");
        tokio::time::sleep(Duration::from_millis(50)).await;
    }
})

biased 作为第一个参数传入,后面跟着 match .. 表达式。整个结构清晰可读,格式化工具也能正常处理。

超时控制:最常见的异步模式

设一个超时,超时了就放弃。这种代码在 Rust 异步编程里到处都是。

tokio_select!(match .. {
    .. if let Ok(result) = fetch_data(url) => {
        println!("got data: {:?}", result);
    }

    .. if let _ = tokio::time::sleep(Duration::from_secs(5)) => {
        println!("request timed out");
    }
})

5. 装进你的项目

用法很简单。在 Cargo.toml 里加一行:

[dependencies]
better_tokio_select = "0.2"

然后在代码里引入:

use better_tokio_select::tokio_select;

如果你嫌每次都写 use 太烦,可以用全局导入:

#[macro_use(tokio_select)]
extern crate better_tokio_select;

最低支持 Rust 1.71 版本,双许可 Apache-2.0 和 MIT。


6. 不止 better_tokio_select 一个选择

better_tokio_select 不是唯一在做这件事的 crate。Rust 生态里还有几个类似方向的尝试。

selectme(v0.7.1):主打性能和公平性。它的 select! 宏语法和 tokio::select! 基本一致,但内部实现做了优化,支持 inline! 宏做更精细的控制流管理。如果你对性能有极致追求,可以看看它。

tokio-alt-select:Dioxus 作者 jkelleyrtp 写的,用闭包语法替代了宏的自定义 DSL。写法更接近普通函数调用,rustfmt 也能处理。不过语法风格差异比较大,需要团队适应。

三个 crate 解决的是同一个问题:tokio::select! 的格式化困境。区别在于语法风格和附加功能。选哪个取决于你团队的口味。


7. 什么时候该换 better_tokio_select

说实话,如果你的 select! 块只有两三行,原版就够了,没必要折腾。

但如果你的代码出现了这些信号,就值得考虑了:

  • select! 块超过 20 行,分支超过 4 个
  • 团队多人协作,格式不统一让人头疼
  • 你用了 cargo fmtselect! 块永远是乱的
  • 代码审查时,格式问题比逻辑问题还多

这些看起来都是小事,但小事积累起来就是摩擦力。好的工具就是帮你消除这些摩擦力。


常见问题

better_tokio_select 和 tokio::select! 性能有区别吗?

没有。better_tokio_select 只是换了一层语法糖,编译后的代码和原版 tokio::select! 生成的完全一样。它不引入新的运行时开销。

为什么不用 selectme?

selectme 是个好项目,但它的语法和 tokio::select! 基本一样,同样存在 rustfmt 兼容问题。如果你的核心需求是格式化,better_tokio_select 更直接。

那些 .. 会不会影响可读性?

刚看确实有点怪,但用几次就习惯了。而且格式化后的代码整体可读性提升,远大于 .. 带来的那点不适。团队里统一用一种风格,比每个人都写不同缩进强得多。