上周末读到 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 到底做了什么

工具本身很小,但五脏俱全:

  1. 从命令行读取城市名;
  2. 调用 geocoding API 拿到经纬度;
  3. 调用天气 API 拿到当前温度、湿度、风速、风向;
  4. 用温度和风速算一个体感温度;
  5. 把结果带点颜色输出到终端。

把这套流程抽象一下,其实就是 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.161.2 MB0.8 s最快1 小时+
Rust3.8 MB(需手动开 LTO + strip)28 s 首次4.8 ms中等
Go6.2 MB<1 s5.2 ms~10 分钟
Bun45 MB(编译后)直接跑 .ts接近 Rust~8 分钟
Node.js~100 MB 运行时直接跑 .ts82 ms 冷启动~8 分钟

第一眼我是站 Rust 的:4.8 ms,二进制压到 3.8 MB,又快又小。但读完作者的实现细节后,我改变了想法。

Rust:快,但你要为"快"付税

Rust 版本用了 clapreqwestserdeserde_jsontokio,总共 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}&current=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.