上周末读到 Sarath S 的一篇文章,标题叫 Rust vs Go vs Zig vs Bun vs Node.js for CLI tools in 2026: I built the same tool five times。我第一眼以为是那种“Rust 天下第一”的俗套评测,结果越读越不对劲——作者最后选的居然是 Go。
关键是,他不是纸上谈兵。他真的花了周末时间,用五种语言各写了一遍同一个天气查询 CLI:wx。功能不花哨:地理编码、天气 API、JSON 解析、体感温度计算、彩色终端输出。但每一步都是真实 I/O,没有 mock 自己的代码。
我顺着他的思路复现了一遍,又把它放到 AI Agent / function calling / MCP tool 的语境里想了想。这篇文章相当于我的实验笔记,也是我对“AI 工具到底该用什么语言写”的一个回答。
wx 到底做了什么
工具本身很小,但五脏俱全:
- 从命令行读取城市名;
- 调用 geocoding API 拿到经纬度;
- 调用天气 API 拿到当前温度、湿度、风速、风向;
- 用温度和风速算一个体感温度;
- 把结果带点颜色输出到终端。
把这套流程抽象一下,其实就是 LLM 调用外部工具时的标准链路:
LLM 决定调用 → 传参 → HTTP → JSON → 小计算 → 格式化返回
差别只在于是人按回车,还是 Agent 替你按。
先看实测数字
Sarath 的测试环境是 M3 MacBook Pro, 18 GB RAM, macOS 26。他用 hyperfine 跑:
hyperfine --warmup 3 --min-runs 50 './wx reykjavik'
网络请求打到本地 mock server,避免真实网络波动。测出来的结果是这样的:
| 语言 | 二进制大小 | 编译/启动 | 完整执行时间 | 开发耗时 |
|---|---|---|---|---|
| Zig 0.16 | 1.2 MB | 0.8 s | 最快 | 1 小时+ |
| Rust | 3.8 MB(需手动开 LTO + strip) | 28 s 首次 | 4.8 ms | 中等 |
| Go | 6.2 MB | <1 s | 5.2 ms | ~10 分钟 |
| Bun | 45 MB(编译后) | 直接跑 .ts | 接近 Rust | ~8 分钟 |
| Node.js | ~100 MB 运行时 | 直接跑 .ts | 82 ms 冷启动 | ~8 分钟 |
第一眼我是站 Rust 的:4.8 ms,二进制压到 3.8 MB,又快又小。但读完作者的实现细节后,我改变了想法。
Rust:快,但你要为"快"付税
Rust 版本用了 clap、reqwest、serde、serde_json、tokio,总共 95 行。代码本身不算复杂:
#[derive(Parser)]
#[command(name = "wx", about = "Weather lookup")]
struct Cli {
city: String,
}
#[derive(Deserialize)]
struct WeatherResponse {
current: CurrentWeather,
}
#[derive(Deserialize)]
struct CurrentWeather {
temperature_2m: f64,
wind_speed_10m: f64,
relative_humidity_2m: u8,
wind_direction_10m: f64,
}
真正让我皱眉的是首次编译。cargo build --release 第一次跑了 28 秒,期间我一度以为 cargo 卡住了,切到另一个终端去查进程。后来发现是 reqwest + tokio 拖进来一百多个传递依赖,每个都要编。
更讽刺的是二进制大小。默认 release 出来是 8.4 MB,要压到 3.8 MB 得在 Cargo.toml 里加:
[profile.release]
lto = true
strip = true
这条配置不是新手会知道的。我自己第一次写 Rust CLI 的时候也是直接 release 出去,根本没想过 8.4 MB 算不算大。
所以 Rust 的问题不是性能,而是你把工具写出来之前,要先交一笔时间税。
Go:无聊,但十分钟后就能用
Go 版本 68 行,零外部依赖。结构体长这样:
type WeatherResponse struct {
Current struct {
Temperature float64 `json:"temperature_2m"`
WindSpeed float64 `json:"wind_speed_10m"`
Humidity int `json:"relative_humidity_2m"`
WindDir float64 `json:"wind_direction_10m"`
} `json:"current"`
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: wx <city>")
os.Exit(1)
}
city := os.Args[1]
// geocode, fetch weather, compute, print
}
Sarath 说他花了大概 10 分钟。我复现的时候也没超过 15 分钟,中间没下载任何依赖,没等任何编译。go build 秒过。
但 Go 有一个老毛病:68 行里你能写 5 次 if err != nil { log.Fatal(err) }。请求一次、读 body 一次、unmarshal 一次、再请求一次、再读一次。这不是 bug,是生活。
resp, err := http.Get(url)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil { log.Fatal(err) }
烦吗?烦。但它就是能跑,而且跨平台编译一条命令:
GOOS=linux go build
Rust 也能做到,但要配 target;Zig 更强,但你要会。Go 的特点是你不需要会,零配置。
Zig:数据漂亮,过程痛苦
Zig 的数据最诱人:1.2 MB 二进制,0.8 秒编译,执行最快。但实现花了 Sarath 一个多小时,108 行代码,是五个版本里最长的。
const WeatherResponse = struct {
current: struct {
temperature_2m: f64,
wind_speed_10m: f64,
relative_humidity_2m: u8,
wind_direction_10m: f64,
},
};
pub fn main() !void {
var debug_allocator = std.heap.DebugAllocator(.{}){};
defer _ = debug_allocator.deinit();
const allocator = debug_allocator.allocator();
// 每个可能分配内存的函数都要传 allocator
}
这段代码看起来还好,但实际问题出在 TLS。Zig 的 std.http.Client 自己实现了 TLS,但它找不到系统 CA bundle,直接报:
error.TlsInitializationFailed
就这一行,没有更多上下文。Sarath 花了一个小时翻 GitHub issue 才拼出解决方案:用 std.crypto.Certificate.Bundle 手动加载证书再交给 client。
另外,写 Zig 时你会反复做一件事:给每个可能分配堆内存的函数传 allocator。写个天气 CLI,总共分配不了几个字符串,但每次都要把 allocator 传来传去。作者形容得很好:“像是走路去 mailbox 还要系安全带”。
还有一个细节: ReleaseSmall 构建没有堆栈跟踪。 release 出来的 1.2 MB 二进制是很美,但调试时你会想念 ReleaseSafe。
我对 Zig 的感情很复杂。语言设计确实漂亮,1.2 MB 的二进制也确实让人心动。但 2026 年,它的生态还没有准备好做一个"周末就能写完的 CLI"。
Bun:写起来最爽,发出去最头疼
Bun 版本 47 行,是五个里最短的。fetch 内置,JSON 一行解析,顶层 await 直接用:
const city = Bun.argv[2];
if (!city) {
console.error("usage: wx <city>");
process.exit(1);
}
const geoRes = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geo = await geoRes.json();
const { latitude, longitude } = geo.results[0];
const wxRes = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,relative_humidity_2m,wind_direction_10m`
);
const data = await wxRes.json();
我写这个版本的时候,大部分时间不是在调 HTTP,而是在写体感温度公式。 Bun 把基础设施全包了。
但发布环节毁所有:
bun build --compile
产物 45 MB。因为 Bun 把 JavaScriptCore 引擎和整个运行时一起打包了。如果你是写给自己用,.ts 文件直接跑,完美。如果你要让用户下载一个可执行文件,45 MB 劝退。
Node.js:和 Bun 一样写,比 Bun 慢
Node.js 的代码和 Bun 几乎一样,区别在运行时。我用 Node.js 24 LTS 跑:
node wx.ts
TypeScript 已经原生支持(erasable syntax only),不用 tsx。但冷启动慢了:空脚本启动就要 82 ms,Bun 只要 24 ms。完整执行下来,Node.js 是 Bun 的十倍左右。
分发方面,Node.js 要么装 ~100 MB 运行时,要么用 Single Executable Application(SEA)打包。SEA 到 2026 年还是实验性功能,产物大小和 Bun 编译差不多。
我的结论和 Sarath 一样:2026 年新开一个 Node.js CLI 工具,等于主动选一个更差的 Bun。
放到 AI 工具调用里再看
现在回到 AI Agent 的场景。一个 LLM 可调用的工具,本质上就是这个 wx 的放大版:
- 收到参数 → 调外部 API → 解析 JSON → 做点小计算 → 返回结构化结果
在这种 workload 下,我对五门语言重新打了个分:
| 语言 | 工具调用效果 | 性能 | 开发体验 | 分发友好度 | 综合推荐 |
|---|---|---|---|---|---|
| Go | ★★★★★ | ★★★★☆ | ★★★★★ | ★★★★★ | 首选 |
| Rust | ★★★★★ | ★★★★★ | ★★★☆☆ | ★★★★☆ | 高维护成本时选 |
| Bun | ★★★★★ | ★★★★☆ | ★★★★★ | ★★☆☆☆ | 内部/个人工具 |
| Zig | ★★★☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★★ | 再等等 |
| Node.js | ★★★★☆ | ★★☆☆☆ | ★★★★☆ | ★★☆☆☆ | 不建议新项目 |
Go 为什么是综合最优?
它不是任何单项冠军,但它是总摩擦最小的:标准库有 HTTP/JSON、编译秒级、二进制单文件可发、跨平台一条命令、性能差 Rust 不到 1 ms。对 AI 工具调用这种"小 I/O + 小计算"的任务,Go 的边际收益刚刚好。
我举个具体场景。你今天要给 Agent 加一个"查 GitHub 仓库 star 数"的工具。用 Go,从 main.go 到一个 gh-stars 二进制,可能 20 分钟;用 Rust,光等依赖编译就要喝半杯咖啡。
什么时候选 Rust?
工具要长期维护、对外分发、不能翻车。比如 MCP server、代码分析工具、安全敏感的工具。Rust 的编译时间税换的是运行时的稳定。
Bun 呢?
如果工具只跑在你自己的机器或团队内部,不对外发布,Bun 是最快乐的。一旦要分发,45 MB 起步的体积就是硬伤。
Zig 和 Node.js
Zig 等 1.0 以后再看;Node.js 新工具可以直接跳过,Bun 把它的活都干了。
我的最终建议
- 想快速落地一个 AI 可调用的工具:选 Go。
- 要做成大项目、长维护周期、对稳定性零容忍:选 Rust。
- 内部脚本、自己用、不对外分发:选 Bun。
- 2026 年还新开一个 Node.js CLI 工具:没必要。
- Zig:值得持续关注,但现在还不是生产力语言。
Rust 很快,也很安全。但在"工具调用"这个具体场景里,它输给了更"无聊"的 Go。这个结果我自己也没想到——毕竟三年前我们还在说"Rust 是 CLI 的未来"。
来源:
- Sarath S, “Rust vs Go vs Zig vs Bun vs Node.js for CLI tools in 2026: I built the same tool five times”, Medium, Aug 2026.
