Rust Hello World破冰:rustup、rustc、cargo三大工具链详解
2026/9/23 8:49:23 网站建设 项目流程

1. 环境准备:Hello World之前的三个关键角色

很多人学Rust,第一件事就是去官网下载安装包,装完发现命令行里敲不出rustc,或者敲出来提示"不是内部或外部命令",然后就开始怀疑人生。这个情况我见得太多,其实根源在于没有搞清楚Rust工具链里三个角色的分工。

先说结论:**Rust的Hello World不是写一段代码那么简单,它背后是三条命令协同工作的结果。**这三条命令分别是rustuprustccargo,很多人把三者混为一谈,其实它们的职责完全不同。

rustup是Rust的版本管理器和工具链安装器,你可以把它理解为Rust世界里的"应用商店"。你用它来安装、更新、切换Rust编译器版本,还能安装不同的编译目标平台。rustc是真正的编译器,负责把.rs源文件编译成可执行文件,类似于C语言里的gcccargo则是包管理器和构建系统,负责创建项目、下载依赖、运行测试、构建发布,相当于"npm + webpack"或者"Maven"的合体。

安装Rust最标准的方式是执行下面这条命令:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

这个命令会下载并运行rustup的安装脚本。如果是Windows用户,需要先去官网下载rustup-init.exe来安装。安装完成后,Rust的路径会被写入到shell配置文件中(Linux/macOS下通常是~/.bashrc~/.zshrc,Windows下是系统环境变量),所以新开一个终端窗口,而不是在安装环境中直接使用,否则你可能看不到命令。

验证安装是否成功,有三个命令可以分别确认三个工具的状态:

rustc --version # 查看编译器版本 cargo --version # 查看包管理器版本 rustup --version # 查看工具链管理器版本

我见过太多人装完直接关掉终端,下次开会话发现命令找不到,其实只是因为环境变量没刷新。你在当前终端执行source ~/.cargo/env或者重新打开一个终端窗口就能解决,根本不用重装。

还有一个常见疑问——Rust支持跨平台吗?答案是支持的,而且支持得相当彻底。你在Windows上写的Rust代码,不需要修改就可以在Linux和macOS上重新编译运行。这得益于Rust的编译模型和标准库设计,以及LLVM后端对多平台的支持。这意味着你写fn main()的那一刻,已经有了向Windows、Linux、macOS甚至嵌入式设备同时输出的潜力,只是当前编译的只是本机版本而已。

在进入真正的代码之前,还有一个常被忽视的点值得多说一句:**写Rust不一定非要安装本地环境。**如果你想快速体验语法、验证一段代码的编译结果,可以打开Rust官方提供的在线Playground(play.rust-lang.org)。浏览器里就能跑代码,支持选择不同版本的编译器,还不占用本地空间。不过我的建议是,想认真学Rust,本地环境早晚都要装,因为后续的cargo构建、依赖下载、调试、项目组织这些核心工作流,Playground是替代不了的。

2. main函数、println!和语句表达式:Hello World背后的三种语言思维

Rust的Hello World长这样:

fn main() { println!("Hello, world!"); }

表面上看,这和其他语言的Hello World没什么两样。但如果你只把它当成一段"每学一门语言都要走过场"的代码,就会错过Rust设计的几个关键点。

**第一,fn main()是Rust程序的唯一入口。**Rust不像Python那样从上到下逐行执行脚本文件,也不像JavaScript那样有一个隐式的入口环境。一个Rust可执行程序,运行时一定会从main函数的开头进入,在main函数的结尾结束。这意味着Rust程序的结构从一开始就是明确的:没有"文件被加载就自动执行代码"这种隐式行为,所有逻辑都显式地从一个main开始。如果你写过C语言,对这个模型不会陌生;如果你是从Python或JavaScript转过来的,第一次编译时可能会困惑"为什么我写在文件顶部的代码没运行",答案就是——Rust没有顶层可执行代码,一切都得在main函数里启动。

**第二,println!带有一个感叹号,这个感叹号不是装饰。**在Rust里,println!不是函数而是宏。宏是Rust里一种"生成代码的代码",它在编译阶段展开成实际的代码,而不是在运行时被调用。为什么Hello World要用宏而不是函数?最简单的理由是灵活性:println!的第一个参数是格式字符串,它支持{}占位符、具名参数、格式控制符等一整套模板语法,这些功能如果做成普通函数,签名会变得极其复杂。宏可以在编译期检查格式字符串和参数是否匹配,比如你写了println!("{}", 42)没问题,但如果你写println!("{}")不传参数,编译就直接报错,这比运行时才发现日志炸了要舒服得多。

**第三,Rust里几乎一切都是表达式。**在main函数中,你可以直接写:

fn main() { let number: i32 = { let x = 10; let y = 20; x + y }; println!("{}", number); // 输出30 }

花括号{}是一个代码块,而代码块本身就是一个表达式,它的值是块内最后一个没有分号的表达式。这个特性在C语言里只会被认为是"块级作用域",而在Rust里它直接参与值的计算。你会在Rust代码里频繁看到这种写法:用let x = if condition { ... } else { ... }而不是先声明再赋值。这是Rust表达式思维和C语言语句思维的区别之一,Hello World虽然没直接展示这一点,但理解它可以帮你避开后续学习中的一个思维惯性陷阱。

如果给Hello World做一点小改动,让它更像"你自己写的第一个Rust程序",可以试试这样:

fn main() { let name = "Rust"; println!("Hello, {}!", name); }

{}占位符会被name的值替换。多行输出可以用println!多次调用,或者在一行里用带参数的格式串。你甚至可以试试eprintln!("错误信息");,它把输出发送到标准错误流,这在调试时比println!更合理——当然这就是后话了。

3. 三种运行方式:rustc直编、cargo构建和在线运行的区别

把Hello World跑起来,其实有三条路可以走。不同路径适合不同场景,选错路径会让你在项目变大之后非常难受。

3.1 方式一:rustc直接编译单个文件

rustc main.rs

运行上面命令后,当前目录下会生成一个可执行文件(Windows下是main.exe,Linux/macOS下是main)。然后运行它:

./main # Linux/macOS .\main.exe # Windows

终端就会输出Hello, world!

这种方式最简单直接,适合验证"编译是否正常""一段代码能否编译通过"。但它有一个致命局限——只适用于单个源文件、无第三方依赖的情况。一旦你引入了任何外部库,或者代码被拆分成多个模块,rustc的调用参数就会变成一个噩梦。你要手动指定所有依赖路径、每个模块的编译选项、链接参数……这实际上是上古时代程序员的工作方式。所以,尽管rustc是最底层的真实编译器,日常开发你却基本不会直接使用它。

3.2 方式二:Cargo创建完整项目

正常的Rust项目,几乎一律通过Cargo创建:

cargo new hello_world cd hello_world cargo run

这三条命令会做以下几件事:

第一,cargo new hello_world创建一个名为hello_world的目录。这个目录里默认包含:

hello_world/ ├── Cargo.toml ├── .gitignore └── src/ └── main.rs

Cargo.toml是项目的清单文件,类似package.json或者pom.xml,用于声明项目的名称、版本、依赖、编译配置等。main.rs是入口源文件,Cargo已经替你写好了Hello World的默认代码。.gitignore是Git的忽略规则文件,Cargo默认把target/目录排除在版本控制之外。

第二,cargo run会自动执行"编译+运行"两个动作。它先把代码编译成可执行文件,放在target/debug/目录下,然后直接启动它。

第三,Cargo默认生成的是二进制项目,而不是库项目。这从字面上看天经地义,因为你要的是一个可运行的程序。但如果日后你想写一个库供其他项目复用,需要在创建时加上--lib参数:

cargo new my_library --lib

这会生成一个src/lib.rs文件,里面会有一行pub fn add之类的示例函数,而不是main函数。二进制的入口是main,库的入口是lib.rs里的公开函数集合,初学者经常在这个地方绕晕。

Cargo.toml的内容大概长这样:

[package] name = "hello_world" version = "0.1.0" edition = "2021" [dependencies]

[package]段声明项目名称、版本和Rust edition(版本纪元)。[dependencies]段用来声明第三方库依赖。这一段的用途现在看着不起眼,等到你引入serdetokio这些库时,一切的复杂性都在这里管理。

3.3 方式三:Playground在线运行

官方Playground(play.rust-lang.org)提供了零配置的运行环境。它有几个优点:不用安装任何东西,支持快速分享代码链接,方便在社区提问时直接贴出可复现的代码。它的运行机制和本地cargo run类似,但隐藏了所有编译细节。

我一般在两种情况下会使用Playground:一是帮群友排查问题,让问题最小化后贴个可复现链接;二是看别人的代码片段时,先丢进Playground跑一遍看看输出是否符合预期。真正常用的本地开发,还是老老实实用Cargo。

3.4 三种方式的选型建议

场景推荐方式原因
验证单个小文件能否编译rustc快,无额外开销
正式个人项目/工程化开发Cargo依赖管理、模块组织、构建配置齐全
快速分享/演示代码片段Playground零配置,可在线运行,方便粘贴交流

以我个人的习惯,Cargo是主战场rustc只是在写简短的测试代码时偶尔用一下。Cargo虽然没有rustc底层,但它带来的组织能力是项目复用的基础。而且cargo run本身也是在调用rustc,只是把参数封装好了,你不需要关心细节而已。

4. 从Hello World出发:Cargo依赖和"如何再次使用库"的困惑

搜"rust下载库怎么再次使用"的人,大概率遇到过这样的场景:看到别人的项目代码里有use tokio::net::TcpListener之类的内容,自己也照着写了一句,然后在本地rustc直接编译,结果报错说找不到tokio这个模块。

这个问题其实非常典型,根源还是没分清rustccargo的分工。**在Rust里,"引入第三方库"和"编译单个文件"是两套不同的流程。**你用rustc直接编译,编译器只能看到标准库和你手动指定的源码;你用cargo构建,Cargo会读取Cargo.toml里的[dependencies],去 crates.io (Rust官方包仓库,类似npm的registry)下载对应库的源码,然后交给rustc编译。

所以,"下载库之后怎么再次使用"这个问题的本质是:不需要手动下载库文件,不需要像某些语言那样往lib目录里丢jar包,也不需要在代码里写相对路径去include。你只需要做两件事:

第一,在Cargo.toml里声明依赖:

[dependencies] rand = "0.8"

第二,在代码里使用use关键字引入:

use rand::Rng; fn main() { let mut rng = rand::thread_rng(); let n: u32 = rng.gen_range(1..101); println!("随机数: {}", n); }

然后执行cargo run

接下来的事就不需要你操心了:Cargo会自动从crates.io下载rand库源码,编译并链接到你的项目中。下次你在新项目里再次声明同一个依赖,Cargo会使用本地缓存,不会重复下载——这就是"再次使用"的正解。

对Hello World这个级别的练习来说,你可能没法立刻体验到这个流程的威力,但理解Cargo依赖管理的工作方式,比会写println!重要得多。因为Rust生态里几乎所有实用功能都来自第三方库:异步网络编程用tokio、数据库操作用sqlx、命令行解析用clap、序列化用serde……你如果停留在rustc单文件的认知模型里,解锁不了任何这些能力。

5. 从源码到可执行文件:Rust的编译处理流程

cargo run执行的背后,其实隐藏着一条完整的编译生产线。尽管你从Hello World里看不到这些细节,但了解它们对定位问题、理解报错信息非常有帮助。

第一步是词法分析和语法分析。Rust编译器被构造成一个管线(pipeline),最前端先把源代码读入内存,拆成一个个token(词法单元),再根据Rust语法规则构建一个抽象语法树(AST)。这一阶段如果代码写得不对,通常会在报错信息里看到类似于"expected expression, foundlet"之类的内容。

第二步是类型检查和借用检查。这是Rust编译器与C/C++编译器区别最大的一步。Rust的编译器会分析每一个变量的生命周期、所有权的转移、借用关系是否合法。Rust代码可以通过编译,不代表代码只有逻辑问题,而是因为编译器已经做了大量的静态分析来保证内存安全。在Hello World里,没有涉及多少所有权关系,但当你开始写结构体、借用引用时,这一阶段会经常跳出来报错。

第三步是生成中间表示和优化。Rust编译器会把AST转成一连串的MIR(Middle-Level Intermediate Representation,中层级中间表示),再做各种优化。如果你用过调试模式(cargo run默认就是调试模式),编译器不会做太多激进优化,编译速度快、便于调试。如果你想发布一个高性能版本,需要执行cargo build --release,这会开启大量优化,编译时间变长,但运行速度更快。

第四步是通过LLVM后端生成机器码。Rust编译器本身生成的是与平台无关的中间码,最后由LLVM负责把它转化为具体平台(x86_64、ARM等)的机器码。这一步就是Rust能够跨平台的关键底层原理:同一份Rust源码,在Windows、Linux、macOS上只需要重新经过LLVM的不同后端编译,就能得到对应平台的可执行程序。

如果你在Hello World之后想知道"Rust为什么能跨平台",最简单的解释就在这一步:跨平台的支持能力是LLVM后端提供的。Rust团队的贡献在于设计了一套安全的静态语义规则,而LLVM承担了生成各平台机器码的重活。这也解释了为什么Rust编译器发布那么频繁更新——它的背后是两个大型开源项目的联动。

6. Hello World之后:async、数据库、桌面和嵌入式方向能做什么

写完并在终端看到"Hello, world!"那一刻,意味着你已经跨过了学习Rust的第一道门槛。接下来怎么走,取决于你想用Rust做什么。

6.1 async异步编程:从同步思维切换到事件驱动

很多刚接触Rust的人看到"Rust async"会本能地联想到JavaScript里的async/await。语法确实有点像,但核心差异非常明显:Rust的async不依赖垃圾回收器,也不依赖单独的运行时线程(不像Node.js那样有一个事件循环)。

Rust标准库本身没有提供异步运行时,你要用社区的事实标准tokio。示例代码如下:

use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { println!("开始异步任务"); sleep(Duration::from_secs(1)).await; println!("等待结束"); }

#[tokio::main]这个宏把async fn main包装成一个可以在异步运行时中执行的入口。你看它没有println!里那些格式化样式,但整体逻辑很直白。async的难点在于理解Future这个类型和.await的语义——Future是一个惰性的任务描述,只有被.await才会推进执行。这和JavaScript的Promise有相似性,但Rust的Future是零成本抽象,这意味着性能可以与线程模型相媲美。

6.2 数据库:用sqlx操作MySQL

很多业务系统都需要连接数据库。Rust里操作MySQL最主流的方案之一是sqlx,它支持编译时查询检查和连接池。

Cargo.toml加入依赖:

[dependencies] sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "mysql"] } tokio = { version = "1", features = ["full"] }

然后写一段带连接池的查询代码:

use sqlx::mysql::MySqlPoolOptions; #[tokio::main] async fn main() -> Result<(), sqlx::Error> { let pool = MySqlPoolOptions::new() .max_connections(5) .connect("mysql://root:password@localhost:3306/test_db") .await?; let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") .fetch_one(&pool) .await?; println!("用户数量: {}", row.0); Ok(()) }

这段代码展示了两个核心概念:MySqlPoolOptions创建连接池,query_as执行查询并返回类型化的结果。你不需要手动管理连接打开关闭,连接池会处理连接复用和超时。为什么强调连接池?因为每次打开/关闭一个数据库连接的耗时远大于复用连接,用pool可以显著提升高并发下的吞吐量。

6.3 桌面应用:Tauri与轻量级GPI

Rust做桌面应用的路径基本上绕不开 Tauri 。Tauri是一个框架,让你使用Rust作为后端、Web技术(HTML/CSS/JavaScript)做前端,打包体积比Electron小很多,内存占用也更低。你可以把Tauri视为"用Rust重写后的Electron",但它不只压缩体积——它把操作系统层的API通过Rust命令暴露给前端,让安全性和性能都得到提升。

Tauri的关键概念是"命令":在Rust里定义函数并用#[tauri::command]标记,在前端通过invoke调用。这种方式类似浏览器里的fetch,但走的是本机IPC通道,没有网络开销。一个简单的示例如下:

#[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }

如果你对纯Rust原生UI感兴趣,可以关注GPUI——这是Zed编辑器团队开源的一款即时模式GUI库。它用Rust直接绘制像素和排版,不依赖系统控件,也不依赖Web渲染引擎。简单说是把GPU渲染、布局引擎和事件处理都压缩在一个库里面,性能和自定义能力非常强,但上手门槛比Tauri高不少,需要先理解它的窗口生命周期、元素树和事件模型。

6.4 嵌入式开发:CH32和其他RISC-V/MCU平台

Rust不仅跑在桌面和数据中心,它还能搞定嵌入式。用Rust开发CH32这种国产RISC-V单片机,已经成为很多硬件工程师的新选择。CH32系列使用沁恒的RISC-V内核,Rust工具链对你本机是x86_64还是ARM不太敏感,关键在于交叉编译:你用本机的编译器生成目标平台(比如RISC-V)的机器码,然后通过烧录器写入MCU闪存。

嵌入式的Hello World不是打印到终端,而是让LED灯闪烁。核心步骤包括配置目标平台(比如riscv32imac-unknown-none-elf)、建立一个no_std环境(因为MCU没有标准库依赖的操作系统接口),然后写一个main函数配置GPIO输出交替翻转电平。这比桌面Rust多了硬件知识的门槛,但优势非常明显:Rust的所有权和借用检查在资源受限的MCU上格外有用,它能在编译阶段杜绝很多内存越界和释放过早的问题,对嵌入式这种调试条件极其艰难的场景,等于是一层免费的防护网。

7. 踩坑经验:我见过最多的Hello World失败现场

写了修改过不知道多少份Rust代码,我总结几个初学者最容易在Hello World阶段踩的坑,希望你能轻松绕开。

第一个坑是编译器报错显示一串乱码一样的字符。例如:

error: expected one of `.`, `;`, `?`, `}`, or an operator here, but this is `!`

如果是Hello World就出这个错,很大的可能是你从网上复制代码时引入了不可见字符,或者把println!写成了println(少了感叹号)。Rust的宏调用必须要有感叹号,少一个符号编译就会报错。报错信息虽然看起来像天书,但其实是定位问题最有效的线索。把错误信息从上到下、从左到右逐行读一遍,通常解决一半问题。

第二个坑是编码问题。Windows上老旧的终端(比如cmd)默认使用GBK编码,而Rust源文件默认是UTF-8。如果你在println!里直接打印中文,可能在终端显示成乱码。这个问题的原因是终端和编译器编码不一致,跟你代码写没写对完全无关。解决办法有两种:一是使用Windows Terminal代替老的cmd窗口,二是把终端代码页切到UTF-8(执行chcp 65001)。不过这个问题在现代终端里已经很少见了,多数情况下你只需要换个终端程序。

第三个坑是把Cargo和别人项目的旧版本混用。有时候你照着教程写Cargo.toml,添加了依赖,然后cargo run报错说需要更新版本。这不是说你写错了,而是crates.io上的依赖版图和教程写的时候发生了变化。最简单的应对是检查项目对应的Rust edition是否是新的。如果用了edition = "2018"的旧项目文件,依赖版本还停留在几年前的,确实会出现兼容性问题;换成edition = "2021"(或2024),然后把依赖版本更新到与当前时间相符的较新版本,问题通常就消失了。

第四个坑,也是最多人问的——"我明明安装了Rust,为什么在IDE里还提示找不到rustc"。这个大概率不是安装失败,而是IDE没有正确识别环境变量。记住一个小窍门:先打开终端,执行rustc --version确认命令行里能用,然后重启IDE。IDE在启动时会读取当前用户的环境变量,安装完Rust后你需要重启IDE或整个系统,如果还不行,检查IDE的配置里是否把工具链路径指向了错误位置。

8. 个人建议:Hello World之后接下来该做什么

如果你已经完整跑通了Hello World,并且理解了这篇文章前面说的Cargo、rustcrustup三者的关系,我可以给你的下一步学习路线提出一个可执行的建议,而不是泛泛的"多练习"。

第一,把打印输出玩出花样。用格式化字符串打印变量、浮点数保留小数、二进制/十六进制输出、左对齐右对齐,这些看似琐碎的功能其实能帮你迅速熟悉Rust标准库的format!宏体系。写几个简单的小练习:打印九九乘法表、打印一个菱形图案、模拟一个简单的计时器输出。这个过程不需要第三方依赖,只靠标准库就能完成,但你会因此熟悉for循环、match分支和字符串格式化这些起步必备的语法。

第二,亲手创建一个多文件项目。Rust的模块系统是很多初学者容易卡住的地方。试试把Hello World里的功能拆成两个文件:main.rs调用lib.rs里的函数。你会发现需要引入mod关键字、理解pub的作用、学会相对路径和绝对路径的引用方式。这个能力决定了你后续能不能写出结构清晰的项目,比急着背语法重要得多。

第三,把Hello World改成一个小工具。比如读取命令行参数并打印出来,或者从文件中读取一行文本再输出。这看似很小的需求,实际上会强迫你接触std::env::argsstd::fs::File、错误处理(Result类型)这些核心概念。等你处理完这些,你会发现自己已经不知不觉地掌握了Rust的内存安全模型和错误传播机制的一角。

第四,给自己安装一个小目标。比如"一个月内用cargo创建一个CLI工具,能够解析命令行参数、读取配置文件、输出结果"。这比沉浸在语法细节里更能拉动你前进。Rust的学习曲线公认比较陡峭,但Hello World已经证明你迈出了第一步。之后的无非是:多写、多编译、多读错误信息、多用cargo管理依赖。当有一天你回头再看println!("Hello, world!"),你会觉得这行代码平凡得理所当然——但那时你已经走了很远。

于我而言,Rust最迷人的地方不是它"快"或者"安全",而是它逼着你在写下每一行代码前就想清楚资源的归属和数据的流向。在Hello World里你可能感受不到这种力量,但从第二个程序开始,这种思维训练就会如影随形。保持动手,保持好奇,这条路值得走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询