


















本文永久链接 – https://tonybai.com/2026/05/27/migrate-go-to-rust
大家好,我是Tony Bai。
在现代后端系统编程领域,Go 和 Rust 无疑是最耀眼的两大双子星。它们都拥有静态类型、编译型、单二进制文件分发等优异特性。然而,这两门语言在底层的设计哲学、运行时权衡以及开发者体验上,走向了截然不同的方向。
Matthias Endler(Corrode 咨询公司创始人)撰写的《从 Go 迁移到 Rust》(Migrating from Go to Rust)是近年来系统编程领域极具深度的一篇迁移指南。作为在生产环境中同时大规模部署过 Go 和 Rust 系统的资深架构师,Matthias 并没有陷入单纯的“谁比谁快”的无意义争论,而是从正确性保证、运行时权衡、工程重构成本等多个维度,客观地为准备进行语言迁移的团队提供了一份极其务实的工程路线图。
以下是该迁移指南的完整简体中文译文,以及技术社区对于此文的精彩技术辩论与观点。

在我协助团队进行的所有迁移中,从 Go 到 Rust 的迁移是一个特例。
这并不是“Rust 会更快吗?”或“Go 是否拥有类型系统?”的问题,因为 Go 在这些方面已经做得很好了。这里的讨论主要围绕正确性保证、运行时权衡以及开发人员体验展开。
在开始之前,先做一个简短的免责声明:本指南高度侧重于后端。后端服务是 Go 的强项所在——小巧的静态二进制文件、专注于网络连接的标准库,以及用于 HTTP 服务器、gRPC、数据库等的庞大生态系统。
这也是大多数考虑使用 Rust 的团队的来源(至少是那些联系我的团队),因此我认为这是在实践中最有用的对比。如果你正在编写命令行工具(CLI)、嵌入式固件或游戏引擎,本文中的一些内容仍然适用,但老实说,我恐怕这不是最适合你的资源。
作为背景,我之前曾写过关于 Go 和 Rust 对比的文章,比如 2017 年的《Go vs Rust?选择 Go》,以及后来与 Shuttle 团队合作撰写的《Go vs Rust:实操对比》,后者通过一个小型后端服务展示了两种语言的具体差异。
你将在本文中学到什么
- Go 与 Rust 的重叠点和分歧点。
- Go 的模式如何映射到 Rust。
- 你能从借用检查器中获得什么。
- 我在什么情况下会建议人们保留 Go,以及在什么情况下 Rust 值得进行迁移。
- 如何渐进式地迁移 Go 服务。
坦白说:我不是 Go 的粉丝。我认为它是一门设计糟糕的语言,尽管它非常成功。它混淆了简单性(simplicity)与易用性(easiness),并且它的几个核心设计折中——无处不在的 nil、作为纪律规则而非类型的错误处理、长期缺失的泛型——都将设计引向了我所不认同的方向。尽管如此,成功才是硬道理!Go 已经捕获了庞大且持久的活跃开发者份额,在 JetBrains 开发者生态系统调查中一直维持在 17-19% 左右。Rust 正在稳步增长,但目前仍然只占一小部分:

Go 显然对很多人都非常适用,而一个假装其不适用的指南是毫无帮助的。因此,在这份指南中,我将尽最大努力保持客观,而不是去重新争论那些老问题。但你应该了解我的先验立场,以便进行校准。
另一个值得披露的前提是:我运行着一家 Rust 咨询公司;所以,我当然是有偏见的!更多人使用 Rust 对我的业务是有利的。但我也在专业领域中使用过这两门语言,并曾将 Go 服务推向生产环境。
本指南适用于那些希望诚实对比迁移到 Rust 时会有什么变化的 Go 开发者。
如果想看一个故意持相反立场的观点,我推荐阅读 Blain Smith 的《就用 Go语言好了,别他妈的废话了!》(Just Fucking Use Go)。在脑海中同时保留这两种观点,比只持其中一种更有用。
如果你更喜欢观看视频而不是阅读,这里有一段来自 The Primeagen 对上述 Shuttle 文章的视频阅读和点评:
(视频:Rust vs Go: Hands On Comparison)
Go 开发者已经拥有了行业内最干净的工具链之一。在很久以前,它就开启了“自带电池(batteries included)”式工具链的潮流,为你提供了一个单一、一致的界面,用于构建、测试、格式化、lint 和管理依赖项。我很高兴 Rust 也效仿了这种做法,因为这是一个极好的模式。这是我最喜欢的这两个生态系统的部分之一。
cargo 甚至拥有更多内置功能:

最大的区别在于,在 Go 中你通常需要借助第三方工具(golangci-lint、mockgen、air、goreleaser)来填补空白。而在 Rust 中,原生(第一方)生态系统开箱即用的功能要丰富得多。有些需要外部 crate 的工具(例如 cargo watch、cargo nextest)只需一个命令即可完成安装并开始使用,例如运行 cargo install cargo-nextest 即可立即获得 cargo nextest。
两个社区在格式化工具上都达成了相同的共识:一个单一的、规范的风格,即使不是完美的,也远比在琐碎的争论(bikeshedding)上浪费时间要好。
“Gofmt 的风格不是任何人的最爱,但 gofmt 却是每个人的最爱。”
— Rob Pike, Go Proverbs
对于 rustfmt 也是如此;并非每个人都喜欢它的每个细节,但代码评审中不再存在关于代码风格的争端,远比偶尔遇到你不喜欢的格式化偏好要有价值得多。

核心结论是,Go 和 Rust 都是编译型、静态类型、单二进制文件部署、具有强大并发能力的语言。不同之处在于编译器向你保证了什么,以及你对运行时行为拥有多少控制力。
在深入探讨之前,有一个概念框架很有帮助:当你从 Go 迁移到 Rust 时,大部分变化都会被推入类型系统。 空值处理、错误传播、数据竞争、资源生命周期、取消机制、泛型,这些在 Go 中要么依赖运行时规范、工具链(go vet、errcheck、golangci-lint、-race),要么依赖运行时的自觉性。而 Rust 则将它们编码为类型,以便编译器在编译时强制执行。
常见的反对意见是这带来了“更多的认知负荷”。我不认同这种说法。我认为,这其实是将认知负荷从你由于必须记住规则而产生的焦虑中释放出来,转移到了编译器身上。一旦你内化了这种模式,并发现它在代码中无处不在(Option、Result、&mut T、Send/Sync、RAII 守卫),Rust 就会停止让你感到沉重,并开始感觉编译器正在为你做你以前必须在大脑中做的工作。
Go 开发者通常不会因为 Go “太慢”而转向 Rust。对于大多数后端工作负载,Go 已经足够快了。人们普遍是对 Go 的一些由于设计不严密而产生的问题感到沮丧:nil 指针带来的隐患、段错误(segmentation faults)的风险、缺乏泛型(长期以来)或任何更复杂的类型系统特性(如枚举和强大的 trait),以及标准库中存在一些怪异的缺失,例如缺少一个内置的 Set 类型(惯用的替代方案是 map[T]struct{},它在实践中行得通,但感觉类型系统并没有真正起到作用)。
你部署了一个 Go 服务,它运行得很好,持续了几个月。然后,某条代码路径被执行,而其中有人忘记检查某个指针是否为 nil,导致 goroutine 崩溃。一个常见的例子是查找操作,它返回零值,或者反序列化后未填充结构体中的某个指针字段:
func (s *Service) Handle(req *Request) error {
// Find 返回 (*User, error)。如果是 "not found",error 为 nil;
// 调用者应该检查 user != nil,但这非常容易被遗漏。
user, err := s.repo.Find(req.UserID)
if err != nil {
return err
}
return user.Account.Notify() // 如果 user 为 nil,或 Account 为 nil,则会发生崩溃
}
Linter 和 IDE 会捕获其中一些情况(通过 nilaway、staticcheck),但它们是选择性开启的、概率性的,而且不能可靠地跨越包边界。Rust 的编译器则根本不允许你忽略这种情况。Rust 的 Option
fn handle(&self, req: &Request) -> Result<(), ServiceError> {
let user = self.repo.find(req.user_id)?; // 返回 Option<User>; ? 运算符进行短路处理
user.notify()
}
如果没有显式处理 None 的情况,你甚至无法解引用一个 Option。一整类导致 pager-duty(线上紧急警报)事件的事故就这样消失了。
go test -race 是一个优秀的工具,但它是一个运行时检测器,意味着它只能找到测试中实际执行到的竞争。在线上高负载下,多个 goroutine 在没有锁的情况下修改同一个 map 会轻松绕过该测试,并导致生产环境崩溃。
在 Rust 中,跨线程共享可变状态需要实现 Send 和 Sync。尝试共享一个普通的 HashMap 并且程序甚至无法编译。你被迫将其封装在 Arc<Mutex<…>> 或 Arc<RwLock<…>> 中,否则编译器会报错。这样,数据竞争在编译时就成了一个类型错误。
Paul Dix 对于什么促使了 InfluxDB 3.0 的重写非常坦诚,而数据竞争的故事就排在最前面:
“【最主要的好处是】无畏并发——消除了此前我们从未消除的数据竞争。在 Influx 1.x 版本中,确实存在一些非常棘手的 bug。”
— Paul Dix, InfluxData 创始人兼 CTO,摘自 Rust in Production
在 Go 中,你会写:
if err != nil {
return err
}
在一两年的开发后,你通常会注意到三件事:
对反方观点保持诚实也很重要,因为在关于我的 Shuttle 文章的 Lobste.rs 讨论线程中,经验丰富的 Go 开发者指出,errcheck 和 golangci-lint 捕获了绝大多数“忘记处理错误”的情况,并且显式的 if err != nil 比深层嵌套的 ? 链更容易阅读。这两个观点都很中肯,显式风格是一个刻意的文化抉择,而不是一次疏忽:
“我认为错误处理应该是显式的,这应该是该语言的核心价值。”
— Peter Bourgon, GoTime #91,引用自 Dave Cheney 的 Zen of Go
我的看法是,lint 是一个你必须记住去配置的选择性安全网,而 Rust 的 Result<T, E> 是类型签名本身,无法被遗忘。样板代码与可读性之间的折中是非常真实且见仁见智的。
在 Rust 中:
#[derive(Debug, thiserror::Error)]
pub enum UserError {
#[error("user {0} not found")]
NotFound(UserId),
#[error("user already exists")]
AlreadyExists,
#[error(transparent)]
Repo(#[from] RepoError),
}
pub fn rename(id: UserId, name: &str) -> Result<User, UserError> {
let mut user = repo::get(id)?; // ? 自动将 RepoError 转换为 UserError
user.name = name.to_string();
Ok(user)
}
? 运算符处理了错误传播,#[from] 处理了类型转换,而针对 UserError 的 match 是穷尽检查的。如果明天你添加一个新的错误变体,编译器会向你展示每一个需要更新的地方。
Go 在 1.18 中引入了泛型,它们很有用,但实现上有一些限制(不支持类型参数上的方法,GC shape stenciling,偶尔会有令人失望的性能表现)。Rust 泛型采用单态化(monomorphize),为每个实例生成具有零运行时开销的专门代码。结合 trait,这为你提供了真正的零成本抽象。
这在处理程序(handler)代码中不那么重要,而在共享基础设施(中间件、通用存储库、解码器、解析器)中更重要,在 Go 中,你常常被迫退回到 interface{} / any 外加类型断言。
Go 的 GC 非常优秀、并发、低停顿,针对典型的服务工作负载进行了很好的调优。但“低停顿”不等于“无停顿”。在重载情况下,P99 延迟尾部明显差于一个不在热路径上分配内存的 Rust 等效程序。
我不会过分夸大这一点,对于绝大多数服务来说,Go 的 GC 根本不是问题。但对于延迟敏感的系统(交易、实时竞价、网络代理、高吞吐量数据摄入),没有 GC 停顿是一个巨大的卖点。Stephen Blum 把它说得很直接:
“Go 在我们的规模下表现很好,但我们确实需要一些能给我们带来高性价比性能的东西,而 Rust 能够让我们达到那个目标。这就是为什么如今基本上所有的东西都在朝着 Rust 发展的原因。”
— Stephen Blum, PubNub CTO, 摘自 Rust in Production
Go 像是遭受了千刀万剐(death by a thousand paper cuts)。它是一门非常实用的语言,如果你愿意忽略上述问题,你可以在其中获得极高的生产力。但在达到一定的代码规模后,问题就会开始累积。Go 失去吸引力并没有单一的瞬间,但团队会发现自己渴望更多(更多的安全性、更多的控制、更多的表现力),这就是他们开始寻找替代方案的时候。
在 Rust 中感到舒适的最快方法是映射你已经知道的模式。如果要看在两种语言中构建相同后端服务的更长、包含大量代码的完整示例,请参阅 Shuttle 对比文章,本节重点介绍最常出现的模式。
Go:
func ReadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
Rust:
fn read_config(path: &Path) -> Result<Config, ConfigError> {
let data = fs::read_to_string(path)?;
let cfg = serde_json::from_str(&data)?;
Ok(cfg)
}
? 运算符替你完成了 if err != nil { return err } 的繁琐工作,如果为 E2 实现了 From
Go:
func GetUser(id string) *User {
for _, u := range users {
if u.ID == id {
return &u
}
}
return nil
}
u := GetUser("123")
fmt.Println(u.Name) // 如果u 为 nil 则会发生panic
Rust:
let user = get_user("123");
println!("{}", user.name); // 编译错误:user 的类型是 Option<User>,而不是 User
// 你必须处理这两种情况:
match get_user("123") {
Some(u) => println!("{}", u.name),
None => println!("not found"),
}
在安全的 Rust 中没有 nil。引用不能是空的。指针可以是空的,但你几乎永远不会在应用程序代码中使用裸指针。
Go 的接口是结构化的,一个类型隐式地满足一个接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
Rust 的 trait 是标称的,你需要显式地实现它们:
pub trait Reader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
}
impl Reader for MyType {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { /* ... */ }
}
Go 的风格非常适合临时性的鸭子类型。Rust 的风格非常适合重构和可发现性,你可以用 grep 搜索某个 trait 的每个实现者。
Rust 中与 interface{} / any 最接近的等价物是 Box
“interface{} 什么也没表达。”
— Rob Pike, Go Proverbs
带有 trait 约束的泛型函数(fn handle
当你确实需要运行时分发(例如,不同实现者的异构存储)时,你会选择 Box
Go 的并发模型以简单著称:
go doWork(ctx, input)
Goroutine 很廉价,运行时会在操作系统线程之间调度它们,而通道(chan T)是主要的协同原语。Go 谚语捕获了这一理念:
“不要通过共享内存来通信;而要通过通信来共享内存。”
— Rob Pike, Go Proverbs
这是 Go 真正大放异彩的地方,并且它对为什么非常明确:在 Go 中,顺序代码和并行代码之间没有语法上的区别。函数签名、它的调用者,或关于它如何编写的任何内容都毫无二致。没有 async fn,没有 .await,没有执行器可供选择,也没有 Send / Sync 约束。只要你不共享可变状态而不进行同步,顺序代码和并发代码看起来是一样的。
这种属性,即没有函数着色(function colouring),是 Go 相比 Rust 最大的日常生产力优势,而在迁移之后,这也是 Go 开发者最怀念的东西。Lobste.rs 讨论中的几位评论者准确地指出了这一点,他们说得很对。Rust 的 async 更加强大且经过更多检查,但它的显式度也更高,这带来了真正的开发体验成本。
Rust 在执行器(对于后端服务几乎总是 tokio)之上使用 async/await:
tokio::spawn(async move {
do_work(input).await;
});
形式很相似。不同之处在于:
对于大多数后端代码,日常体验是类似的:启动一个任务,通过通道进行通信,并大方地使用超时。
在 Go 中,你将 context.Context 传给每个阻塞调用:
func (s *Service) Fetch(ctx context.Context, id string) (*User, error) {
return s.client.Get(ctx, "/users/"+id)
}
Rust 没有内置的 context.Context。最接近取消的等价物是 tokio_util::sync::CancellationToken:
pub async fn fetch(&self, token: CancellationToken, id: &str) -> Result<User, FetchError> {
tokio::select! {
_ = token.cancelled() => Err(FetchError::Cancelled),
res = self.client.get(&format!("/users/{}", id)) => res,
}
}
对于超时,tokio::time::timeout(dur, fut) 可以包装任何 future。对于截止时间/值,你通常将它们作为显式参数传递,或者使用 tracing span 而不是单一的上下文对象。
一些 Go 开发者怀念 ctx 的隐式感。但在实践中,显式的 Rust 风格更容易让人推断,因为你总是确切地知道什么是可以取消的,什么是不可以的。更深层次的观点是,没有任何一种语言可以免费给你取消机制,只是规约出现在不同的层面上:
“Go 并没有办法告诉一个 goroutine 退出。没有停止或杀死函数,这是出于充分的理由。如果我们不能命令一个 goroutine 退出,那么我们就必须礼貌地请求它。”
— Dave Cheney, The Zen of Go
在 Go 中,这种“礼貌地请求”是通过约定俗成地在每个调用点传递并检查 context.Context。在 Rust 中,则是 CancellationToken(或 watch 通道)传给每个调用点,但编译器实际上可以在你忘记时提醒你。
两种语言都有通道。翻译很直接:
Go:
ch := make(chan int, 10)
go func() {
ch <- 42
}()
v := <-ch
Rust:
let (tx, mut rx) = tokio::sync::mpsc::channel::<i32>(10);
tokio::spawn(async move {
tx.send(42).await.unwrap();
});
let v = rx.recv().await.unwrap();
Rust 的通道将发送端(Sender)和接收端(Receiver)区分为不同的类型,这使得所有权和 Send 属性在类型层面是显式的。
Go:
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
Rust:
pub struct Circle {
pub radius: f64,
}
impl Circle {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
Rust 的 &self 相当于 Go 的值接收者;&mut self 是一个带有修改权限的指针接收者。拥有的 self(消耗该值)在 Go 中没有对应物,但在(类型状态、构建器)模式中偶尔非常有用。
Go 的 string 是一个具有赋值时拷贝语义的 UTF-8 字节切片(头部被复制,底层数据是不可变且共享的)。Rust 将其分为两种类型:
作为一条经验法则,参数中接收 &str,在生成新数据时返回 String。
fn greet(name: &str) -> String {
format!("Hello, {name}")
}
一旦你内化了这一点,这基本上是无痛的。&str 与 String 的划分是 Rust 更广泛的“借用与拥有”模型的一个缩影。
Go 在 1.18(2022 年 3 月)引入了泛型,在语言出货十三年之后。它们很有用,但由于它们是后期补丁(tacked on),在实践中它们具有大多数你期望从 Rust、Haskell 甚至现代 C++ 获得的泛型系统的缺点,却没有任何优点。
这是一个很强烈的说法,所以让我来支持它。
最明显的信号是,在泛型落地三年后,Go 自己的标准库仍然主要避免使用它们。sort.Slice 仍然接受一个 func(i, j int) bool 闭包,而不是 cmp.Ordered 约束。sync.Map 仍然被类型化为 any / any。除了 slices、maps 和少数组件外,几乎没有它们的身影。
公平地指出,向后兼容性是这里的主要原因:Go 1 的兼容性承诺意味着现有的非泛型 API 无法重构,因此任何泛型版本都必须与其并存(或在新的包中)。但这只是解释的一部分。已经有足够的时间来引入泛型替代方案,而几乎没有出现这一事实表明语言设计者并不倾向于将泛型作为他们使用的主要工具。
将其与 Rust 进行对比,在 Rust 中,泛型从第一天起就渗透到了标准库中:Option
在 Go 中,泛型是库作者在确实需要时才选择使用的功能。在 Rust 中,它们是构建一切事物的底层基石。
Rust 的泛型与 trait 绑定,trait 兼作该语言进行多态、超类、关联类型、毯子实现(blanket impls)和一致性的机制。
Go 的约束只是带有一个额外 ~ 运算符的接口,用于类型集成员资格。这里没有:
实际的后果是,当你的抽象需要不仅仅是一个“适用于任何 T 的函数外加这几个操作”时,Go 就会迫使你退回到 any 以及类型断言、代码生成或运行时反射。
Rust 使用 Hindley-Milner 风格的推导引擎,可以跨整个表达式传播类型信息,包括跨闭包、迭代器链和 ? 运算符。你经常写:
let evens: Vec<_> = (0..100).filter(|n| n % 2 == 0).collect();
而编译器会推断出 _ 是 i32,而 Vec<_> 目标是 Vec
Go 的推导要浅得多。它通常可以推断出函数参数的类型,但它不能从返回位置上下文中推断,不能通过泛型构建器跨链推断,并且经常在调用处强制使用显式的类型参数:
result := slices.Collect[int](iter) // 经常需要
在 Rust 中这是例外;在 Go 中这仍然很常见。
泛型没有免费的午餐:你必须要么在编译时买单,要么在运行时买单,要么通过代码膨胀(JIT)买单。C++ 和 Rust 在编译时通过单态化买单。Java 在运行时通过装箱买单。Go 选择了折中路线,采用了 GC 形状模板和字典,这有一篇众所周知的 PlanetScale 文章正好展示了这一点。
Rust 进行单态化:每个 Vec
这是最让我困扰的部分。
一个好的泛型系统可以消除退回到逃生舱口的理由。在 Rust 中,泛型 + trait 消除了你对 Box
在 Go 中,泛型并没有消除 any,没有消除 reflect,没有消除代码生成作为诸如 ORM、解码器和 mock 等事物的首选模式。encoding/json 仍在使用反射。database/sql 仍在使用 any。mockgen 仍会生成代码。如果泛型系统能够大放异彩,最应该发挥作用的地方,正是 Go 在 1.18 之前就伸手去拿运行时机制的那些地方。
Go 中的泛型感觉是累加的,只是箱子里的一个新工具,在狭隘的案例中很有用。Rust 中的泛型感觉是基石般的;将它们移去,语言就会崩溃。
这就是区别所在,也是为什么在我的经验中,泛型 Go 代码读起来并不比它取代的基于 interface{} 的代码好;它只是读法不同,有更多标点符号罢了。

如果你已经在 Go 中有了自己的偏好,Rust 生态系统已经趋于相似级别的“默认选择”。对于一个典型的后端服务:axum + sqlx + tokio + tracing + serde + clap 覆盖了你 90% 的需求。
我想坦率地说。从 Go 过来,你将会碰壁。这堵墙有一个名字。
Go 的运行时替你处理内存和别名。Rust 将这个决定推入类型系统。前几个星期你会写出“显然应该工作”的代码,然后编译器会拒绝它。
最常困扰 Go 开发者的模式有:
在所有的这些规则下,借用检查器确实听起来像一个“守门人”,不断阻碍,并且让人感到沮丧。但是,当你开始使用 Rust 时,不应该带着那样的心态。借用检查器真正揭示了你代码中现有的非常真实、非常微妙的 bug,如果你不解决它们,你的程序就会存在安全问题。因此,每当你从 rustc 得到编译器错误时,请退后一步,问自己以下几个问题:
这就是你需要理解借用检查器的心态。人类在推理内存方面真的很糟糕。我们很容易忘记指针可以为空,忘记旧的引用可以比它们指向的数据存活得更久,忘记多个线程可以同时修改同一块数据。我们倾向于对数据在程序中如何流动有一个“线性”的心理模型,但现实中它更接近于一个具有多条路径和交互的复杂图形。每一个 if 条件都会强制你考虑这两种分支中会发生什么。这正是借用检查器旨在为你做的事情!它强制考虑那些极其罕见但确实存在的、当你觉得可能不会发生但就是发生了的代码路径。
借用检查器其实是一个巨大的解脱。一旦它通过了,你就知道你的内存状态是 100% 连贯的,你可以专注于更高层次的问题。这也就是 Ed Page(clap 的维护者)说的:
“当你们刚开始接触它时:会感到沮丧。它让我想起了第一次学习编程的感觉,因为它太不一样了。由于借用检查器和生命周期,我不想去处理那些东西——但我被迫去了。”
— Stephen Blum, CTO, PubNub, 摘自 Rustacean Station
“……能够专注于更高层次的问题。在我进行自我分析并失败时,它帮助我发现了问题。”
— Ed Page, 摘自 Rustacean Station: clap with Ed Page
对你的团队保持诚实,Rust 的编译时间相比 Go 的近乎瞬时的编译确实是一个退步。对于中等规模的服务,全新发布构建可能需要几分钟。增量构建和 cargo check 是合理的,并且编译时间在这些年里已经好了很多,但你仍然会感觉到差异。
为了缓解这种情况,在你的编辑循环中使用 cargo check,在项目见效后将其拆分进 workspace 中,并让你自己的 crate 中不要包含过程宏(proc-macro-heavy)重度依赖,这样它们就只在发生变化时才重新编译。请参阅《加速 Rust 编译时间的技巧》以进行更深入的探讨。
正如《Goroutine 对比 异步任务》中所讨论的,Rust 的 async fn / fn 拆分是从 Go 迁移过来时最大的开发体验退步之一。异步 trait 自 Rust 1.75 以来已经稳定,但在将它们与动态分发结合时,仍然存在一些粗糙的边缘,你偶尔需要借助 async-trait crate 来解决。
Rust 的 crate 生态系统正在增长,并且库在整体上具有很高的质量,但 Go 在一些后端相邻领域具有领先优势:Kubernetes operator、云提供商 SDK、某些特定生态系统的数据库驱动。在做出承诺之前,请花一天时间检查你依赖的库是否具有你愿意使用的 Rust 对应物。我协助的团队经常不得不自己动手实现至少一两个核心库——例如,他们可能需要更新一个废弃的 XML 架构验证 crate,或为较少人知的协议编写自己的客户端。
你不需要一次性重写所有内容。我听到的每一个成功的 Go 到 Rust 迁移案例都是战术性的,而不是大爆炸式的重写。Microsoft 的 Victor Ciura 总结得很到位:
“我们并不是疯狂地到处为了好玩而用 Rust 重写一切。我们在做出这些战术性选择,我们会说:好的,这个新组件,如果我们用 Rust 编写会更好。”
— Victor Ciura, 首席工程师, Microsoft, 摘自 Rust in Production
最有效的策略,按照我通常推荐的顺序如下:
如果你的系统中某个特定服务一直存在各种问题(比如高 CPU 使用率、对延迟敏感,或者经常出现可靠性问题),那么你可以只用 Rust 重新编写这个服务,同时保持与原有 API 的兼容性。这是风险最低的迁移方式。其他用 Go 编写的服务仍然可以通过 HTTP/gRPC 与这个服务进行交互,而无需关心其底层编程语言是什么。Radar 公司的 Jeff Kao 指出,Discord 上的那些成功案例往往能激发团队尝试这种迁移方式的勇气。
如果你在 Hacker News 上搜索“迁移到 Rust”,第一个搜索结果一定是关于 Discord 从 Go 语言切换到 Rust 的报道。这一消息激励了我们,让我们也想看看自己是否也能做到同样的事情。
——Radar 公司的首席技术官 Jeff Kao 谈 Rust 在实际生产环境中的应用
后台任务、队列消费者、数据摄取管道以及那些依赖 CPU 处理的批量作业,都是绝佳的优化目标。这些任务通常具有明确的输入/输出边界(比如队列或主题),且不会与系统的其他部分共享任何状态信息。
可以通过 cgo 在 Go 语言中调用 Rust 代码,关于如何操作的详细指南也很容易找到。(如果你需要我提供相关的指南,请随时联系我。)不过,实际上我并不推荐将 Rust 用于后端服务。与“直接创建一个 Rust 服务并将其置于网络调用之后”相比,其构建的复杂性以及 FFI 相关的开销通常会超过其带来的好处。不过,对于库和 CLI 工具来说,使用 Rust 则更为合适。
如果你使用了 API 网关或反向代理,就可以将特定的端点指向新的 Rust 服务,而其余部分则继续使用 Go 语言来实现。当某个特定的业务领域(如身份验证、搜索、计费)适合被迁移时,这种做法尤为有效。这种模式通常被称为“绞杀者模式”:新服务会逐渐取代旧服务,最终完全取代它。
并非所有东西都需要被迁移。Go 语言在以下方面表现优异:
这并非什么小众职位。对于一家能够大规模提供这两种语言服务的公司来说,这一职位的设立显然意味著更重要的意义:
Go 语言是构建网络服务的绝佳选择。在 Canonical 公司,我们大量使用 Go 语言来开发软件——Juju 就是一个由 Go 语言编写的庞大软件项目。
——Canonical 公司工程部副总裁 Jon Seager 谈 Rust 在现实生产环境中的应用
混合策略其实很不错,也很常见。与我合作的许多团队都会采用这种策略:对于那些“没什么特别要求”的服务,使用 Go 语言来开发;而对于那些需要确保可靠性和性能的服务,则使用 Rust 语言来开发。
根据工作量的不同,具体数字会有很大差异,因此这些数据仅供参考而已。请不要把它们当作绝对的承诺!不过,以下是我在协助进行从 Go 语言到 Rust 语言的迁移过程中所得到的一些大致数据:
“我不需要去追踪崩溃,或者某些奇怪的多线程竞争条件,或者其他那些实际上消耗了我之前大部分时间的事情。”
— Andrew Lamb, 软件工程师, InfluxData, 摘自 Rustacean Station: Rebuilding InfluxDB with Rust
说实话,与从 Python 转向 Rust 相比,从 Go 转向 Rust 后,很难实现 10 倍的性能提升。不过,你确实能减少“愚蠢的错误”,降低延迟,同时还能继续使用同一种语言来开发嵌入式系统或进行系统编程。这往往是代码迁移带来的最令人惊喜的副作用:那些原本需要使用不同编程语言的团队,现在可以共享代码了。Rust 几乎可以用于所有类型的开发场景。
从 Go 迁移到 Rust 是与从 Python 或 TypeScript 迁移完全不同的一种类型。从 Go 过来,你深知静态类型、编译型语言的好处。所以你并不是在用动态类型或缓慢的运行时去交易。你是在交易 nil,换来一个漏洞更少、更健壮的代码库、更严格的编译器(可在编译时捕获更多错误)。不过,这里有一条更陡峭的学习曲线。
对于基础服务(你的组织所依赖的、需要极高可靠性、对你的业务至关重要的服务),这个迁移方式显然是值得的。对于其他服务,Go 仍然是正确的答案。迁移的目的是在最适合的语言中解决对应的问题。
准备好迈向 Rust 了吗?
我协助后端团队评估、规划并执行 Go 到 Rust 的迁移。无论你需要架构评审、培训,还是协助将关键服务进行移植,让我们聊聊你的需求吧。
Matthias 的这篇文章在 Hacker News 上也引发了热烈的辩论。支持者、怀疑者、以及拥有多年双语言实战经验的系统架构师们纷纷下场,就 Go 与 Rust 的工业级博弈分享了大量第一手观点。我对其中的核心争议与洞察进行了系统性汇总:
在 HN 的讨论中,社区普遍赞同的一个终极共识是:Go 与 Rust 的选择,90% 程度上取决于你是否想要一个托管运行时(垃圾回收,GC)。
编译速度是 Go 阵营攻击 Rust 最锋利的武器之一。
在错误处理上,两个阵营各执一词,表现出截然不同的“开发文化”:
开发者对两门语言的第三方生态设计表现出了明显的温度差:
这是一个极具 2026 年时代特色的前沿议题。讨论区多位开发者分享了在使用大模型(如 Claude Code、Cursor)编写这两门语言时的反差体验:
* AI 写的 Rust 质量低下:由于 Rust 的生命周期(Lifetimes)和借用规则极度精密,AI 经常会生成那些无法通过编译的“幻觉代码”,试图滥用 Mutex、RefCell 等高级特权,或者在多线程中引入生命周期冲突。
* 但 Rust 拥有最强“安全网”:然而,反直觉的是,很多开发者表示他们更喜欢让 AI 写 Rust 而非 Go。因为如果 AI 写的 Go 逻辑错了(比如漏了 nil 检查或并发读写未加锁),代码依然能完美编译通过,并在生产环境中引发极其隐蔽的线上故障。而在 Rust 中,“只要 AI 写的代码能通过编译器的金睛火眼,我们几乎就可以闭着眼睛放心地把它部署上线。”
Go 和 Rust 的博弈,本质上是“高带宽易上手的生产效率”与“编译期极致安全的正确性承诺”之间的路线之争。
如果你正在构建一个高速迭代、团队规模庞大、需要快速抢占市场的业务系统,Go 依然是那张最稳健、最不容易出错且极其务实的船票。
但如果你的系统已经走过了野蛮生长阶段,开始面临极其严苛的 P99 停顿要求、高并发下的内存与 CPU 账单压力,或者是不容许有任何运行时恐慌(Panics)的国防级、金融级系统,那么正如 Matthias 团队所验证的那样,忍受 Rust 的学习曲线和编译成本,将为你换来长达数年、在睡梦中都无比踏实的“终极安全感”。
资料链接:
还在为写 Agent 框架频频死循环、上下文爆炸而束手无策?我的新专栏 《从0 开始构建 Agent Harness》 将带你:
扫描下方二维码,开启从 0 开始构建Agent Harness 的实战之旅。

原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!
我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里,你将获得:
衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.
Related posts:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。