一只土拨鼠遇到一只螃蟹。
A Gopher Meets a Crab

原始链接: https://miren.dev/blog/gopher-meets-crab

## Rust 开发者视角 在与 Go 共事十年后,一个最近的项目——为 TokioConf 构建聊天服务器——最终促使我深入研究 Rust。借助 Claude 作为一位耐心的“Rustacean”结对程序员,我探索了 Rust 的优势和不同之处。 关键收获是对 Rust 详尽的枚举的强烈赞赏,它提供了编译时安全保障,而 Go 只能通过测试或代码生成来实现。`?` 运算符简化了 Go 冗长的错误处理,但由此产生的隐式控制流感觉有些不安。复杂的类型注解,尤其是在异步函数中,最初令人困惑。 一个显著的区别在于运行时可见性。Go 的运行时在很大程度上是隐藏的,而 Rust 需要显式选择和集成像 Tokio 这样的运行时,从而暴露其内部运作。这种透明度,最初显得复杂,最终揭示了 Go 是一种更隐式的语言。像 `tokio-console` 这样的工具进一步突出了这一点,提供了 Go 的基于快照的性能分析中无法获得的实时运行时监控。 虽然享受学习体验,但我仍然对 Go 在典型工作负载中的权衡感到满意,但也承认未来偏好可能会发生转变。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 一只掘土鼠遇到一只螃蟹 (miren.dev) 8 分,由 radimm 2 小时前发布 | 隐藏 | 过去 | 收藏 | 讨论 帮助 考虑申请YC 2026年夏季项目!申请截止至5月4日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

I’ve been writing Go for about a decade, and for most of that time Rust has been the language I respected from a polite distance. I bought the book, thumbed through it, but mostly it sat on the shelf. I gave rustlings a try once or twice but always ended up wandering away. I never had the hook to really motivate me to dig in. Miren is at the inaugural TokioConf this week (I’m writing this from the plane there), and my desire to cook up a decent demo presented the perfect opportunity.

So I built a chat server. Well, okay, I told Claude to build a chat server, and then spent a long time asking Claude about what the heck it just wrote. Treated it like my Rustacean pair I could bug to explain things to me. Not nearly as good as a human, but patient, and available over airplane wifi. It’s in our repo full of sample apps, running live at chat.miren.toys. Swing by this week and you might find a few friendly strangers already typing.

A screenshot of the rust-chat UI with three connected users saying hello to each other

Notes from a gopher

So now I’ve spent some honest hours trying to fit Rust into a Go-shaped brain. Here’s what fit nicely and what popped back out the other side.

Exhaustive enums are the thing I’ve wanted in Go for years. In Go, I either reach for codegen or write a test case any time I want to prove I handle every variant of a type. Rust’s compiler just checks. Add another variant tomorrow and every match in the codebase that doesn’t cover the new case lights up red at compile time. I can stop writing tests for my own forgetfulness.

The ? operator is kind of a revelation to an if err != nil-addled brain like mine. One character for what used to be a block. Though I’ll admit, the number of implicit exits from a function still makes me a little itchy. It’s an interesting spot where the two languages diverge on what’s worth making explicit. Go wants you to see every return path, even the sad ones. Rust trusts the type system to carry it instead.

The first line of Rust that made me laugh out loud:

match tokio::time::timeout(Duration::from_secs(2), receiver.next()).await {
    Ok(Some(Ok(Message::Text(t)))) => { /* happy path */ }
    _ => { /* anything else */ }
}

Reads like a nervous little parser muttering to itself. “Ok… some… ok… message… text!” Each layer hoping the next one holds. My reaction to Claude was “do rust-heads not blink at a line like that?” Answer: yes, a little. This is on the border of “clever” and “just write it flat.”

We refactored to lean on ? instead of the big match clause, which gets you to a slightly cleaner helper:

let msg = tokio::time::timeout(Duration::from_secs(2), receiver.next())
    .await
    .ok()?
    ?
    .ok()?;

That unfortunately just swaps out the nervous parser for a slightly deaf one. “Ok?” ”…?” “Ok?” Three tries at the answer, cupping its ear a bit more each time. It still reads funky to me, but to be fair it’s doing the work of about five nested if err != nil branches in Go, one of which I would absolutely forget.

The first line that made me swear:

async fn send_json<S>(sender: &mut S, msg: &ServerMessage) -> Result<(), ()>
where
    S: SinkExt<Message> + Unpin,
    S::Error: std::fmt::Debug,

I stared at these Curry-Howard crimes and typed a series of expletives at Claude that were in an original draft of this post but we decided to elide to preserve a sliver of corporate dignity. Claude patiently walked me through it, and it made sense piece-by-piece but absolutely did not stick. Please do not ask me to explain that snippet of code to you. At the end, Claude conceded that for an app, we could just write the concrete type here. So I backed away slowly. Go generics can get mind-bending pretty quickly too, but maybe not as quickly.

The runtime in plain sight

Rust’s async runtime isn’t part of Rust. There’s no go keyword, no built-in scheduler, no goroutines. The language gives you the syntax for async (async fn, .await, futures as types) and then tells you to go find a runtime that will actually poll them. Tokio is one of those runtimes. You import it into your app.

In Go, that whole apparatus is the language. You type go func() and the scheduler takes it from there. Goroutines, GC, stack growth, preemption: all of it lives under a carpet you can’t really lift.

Rust yanks the carpet. The runtime doesn’t hide its shape from me; the language insists I spell more of it out. That’s what Tokio is: not a feature bundled into the language, but a library I imported, with primitives I can watch and poke at directly.

It took me a while to realize this is what I’d been reading as verbosity. Go is verbose at the surface and quiet underneath. Rust is dense at the surface and quiet nowhere. I did not expect to come home thinking of Go as the more implicit language. But here we are.

Now watch this

That’s where tokio-console comes in. Go has pprof and it’s great; tokio-console live-tails the runtime rather than snapshotting it, so every task, channel, and mutex is a row you watch tick in real time.

Wiring it in took three lines of Rust and a service-port declaration in .miren/app.toml:

[[services.web.ports]]
port = 6669
name = "console"
type = "tcp"
node_port = 30669

Then, from the plane:

$ tokio-console http://chat.miren.toys:30669

tokio-console connected to the live deployed rust-chat instance, showing 11 tasks including three symmetric trios, one per connected WebSocket client

Three humans in the chat room showed up as three trios of tasks: an extract, a send, a receive per client. The axum accept loop had spent 29 microseconds doing work across ten minutes of life (0.000005%), which is what async looks like when it’s working. And my code showed up in the console as naturally as Tokio’s own internals.

Not yet

I had fun finally learning some real Rust, but this old gopher hasn’t suddenly transformed into a crab. The trade-offs Go makes serve the kind of work I do most days. But I hear that over a long enough time span, chances are, it’ll happen.

联系我们 contact @ memedata.com