☰
Rust Hello World深度实战:环境搭建、编译运行与格式化输出全解析
2026/10/9 21:43:05 网站建设 项目流程

学 Rust 的朋友,第一行代码基本都是从 Hello World 起步的。《通过例子学Rust》(Rust by Example)这本官方风格的小册子,把 Hello World 放在了第 1 章,看起来只是打印一句问候语,其实里面埋了三个关键问题:程序从哪开始执行、println! 为什么带感叹号、Rust 的工具链是怎么把源码变成可执行文件的。这篇文章不打算照抄书里的例子,而是把这一章掰开揉碎,结合我实际写 Rust 这两年的经验,把环境安装、编译方式、格式化输出和最常见的报错一次讲透。准备入坑 rust 语言入门、以后想去折腾 Tauri 桌面应用或者 AI Agent 框架的朋友,这篇就是你踩下去的台阶。

1. Hello World 背后的两个核心概念

1.1 为什么是 fn main:程序的入口约定

写任何 Rust 程序,第一行基本逃不开 fn main()。这个入口约定跟 C 语言一样,编译器在链接可执行文件的时候,必须找到一个叫 main 的函数,然后从它的第一行开始执行。区别在于 Rust 的写法更明确:fn 是关键字,后面跟函数名,小括号表示无参数。你要是写个 hello.rs 却不放 main,编译器会直接告诉你 error[E0601]:mainfunction not found in cratehello。

新手经常困惑的点是:既然是第 1 章,为什么框架要用得这么重?一个空壳 main 有什么意义?我的理解是,Rust 是一门需要你在很小的地方就建立正确心锚的语言。main 不写,程序连编译都通不过;写错了位置,编译器也会追着报错。它逼着你从一开始就养成"按约定办事"的习惯。后面你写库、写测试、写二进制工具,入口可能变成 lib.rs、#[tokio::main] 等等,但核心思想都一样:程序总有一个明确的起点。

这里我还想提一个 Rust 和其他语言不太一样的内置习惯:main 函数内的代码块是有作用域的,最后的表达式(不带分号的表达式)会被当作返回值。Hello World 阶段暂时用不上,但第 2、3 章你会频繁用到这个特性。现在只要记住,普通函数在 {} 内部定义,语句以分号结尾,唯一容易被忽略的是"表达式后面没有分号"这件事,先埋个印象。

1.2 println! 为什么带感叹号:宏与函数的差别

再看 println!("Hello, world!"); 这一行,新手第一反应是"函数调用为什么要加感叹号"。这是 Rust 很有辨识度的语法细节:带 ! 的调用是宏(macro),不是普通函数。println! 在被编译的时候会做一些"展开"工作,把你想打印的内容和占位符转换成一套复杂的内部调用,最终落到 std::io::stdout() 上。

为什么不让它当一个普通函数?因为 Rust 的格式化参数是可变的,函数签名很难优雅地表达"前面是格式字符串,后面跟着任意类型的一堆参数"。另外宏可以在编译期做类型检查,比如 println!("{}", 42) 没问题,println!("{}", vec![1,2,3]) 就会报错,因为 Vec 没有实现 Display。这套检查在编译期完成,错误早暴露,运行时不背锅。

与 println! 对应的还有 print!,差别只是末尾不换行。如果你连续 print! 两次,输出会挤在同一行。这两个宏在 Rust 里属于 std::fmt 这块,是使用频率最高的工具之一。以后写测试、写日志库、做 CLI 输出,全都要跟 fmt 打交道。所以第 1 章看起来是个"你好世界",实际是在给你提前铺 fmt 模块的路。

2. 从零搭建 Rust 环境:安装与工具链

2.1 rustup 是官方推荐,先把它装明白

要跑 Hello World,得先把 rust 安装搞定。官方推荐的安装方式是 rustup,它不只是装个 rustc 编译器,而是负责管理整个 Rust 工具链:rustc(编译器)、cargo(包管理器)、rustup 自带的文档和组件。Windows 上直接下载 rustup-init.exe,macOS 和 Linux 用终端执行安装脚本。装完通常会自动把 ~/.cargo/bin 加进 PATH,新开的终端窗口才能用 cargo 和 rustc 命令。

装完之后我先建议你跑一句 rustup show,看看当前 toolchain 的版本和默认目标。官方默认装的 stable 工具链通常够用;如果需要跑 nightly 特性,再用 rustup toolchain install nightly 装第二套。初学者别急着追 nightly,稳定版就是最好的版本。Windows 上还有一个容易忽略的点:如果你要编译原生程序,建议把 Microsoft C++ Build Tools 装上,否则链接阶段会报 link.exe not found 之类的错误。这个坑我在第 5 节再展开,但安装的时候顺手解决是最舒服的。

2.2 编译与运行:rustc 直接上还是交给 Cargo

装好环境,运行 Rust 程序有两种主流方式。最直接的是 rustc hello.rs && ./hello(Windows 上是 hello.exe),编译器会在当前目录生成可执行文件。这种方式适合一两个文件的实验,比如《通过例子学Rust》前面几章。另一种更接近日常开发的是 Cargo:cargo new hello --bin 建项目,编辑 src/main.rs,cargo run 一键编译运行。

这两者的差别我列个表:

对比项rustc 直接编译Cargo 项目
上手成本最低,只有一个文件需要初始化目录结构
依赖管理不支持,要手动找库Cargo.toml 声明即可
构建缓存每次全量编译增量编译,快很多
适用场景学习语法、快速验证真实项目、多文件工程

我的建议是:第 1 章阶段你两种都试一遍,眼睛会了之后,后面统一用 Cargo。因为从第 2 章起几乎每个例子都可能引入第三方 crate,没有 Cargo 你根本玩不转。Cargo 同时承担了 build 和 test runner 的角色,cargo test、cargo fmt 都是它管的。

顺便说一句,Cargo 初始化的目录结构初看很啰嗦:src/main.rs、Cargo.toml、target,还有 Cargo.lock。但这是 Rust 社区统一的项目骨架,学得快的话,整个工程布局一开始就在你脑子里有了。后面你用 Tauri 或者写 CLI 工具,拿到的第一眼也是这套结构,越早习惯越好。

2.3 装完先跑几个命令,确认环境真的可用

环境装完不是等它"看起来能用",我习惯先跑一段验证链。第一个命令是 rustc --version,看到类似 rustc 1.xx.x (xxxxxxx xxxx) 的输出就算通。第二个是 cargo --version。第三个就是我们的重头戏:建一个临时目录,写 hello.rs,编译运行。

如果提示 cargo 不是内部或外部命令,大概率是 PATH 没生效,重开终端或者手动把 %USERPROFILE%.cargo\bin 加进去。如果提示 link.exe not found,去装 Build Tools。如果运行起来没有任何输出但也没有报错,检查一下是不是用了 print! 而不是 println!,或者代码里压根没写 main。

这里我想额外提一下验证环境时最容易踩的"双工具链"坑:有的同学电脑上曾经装过 Rust,后来卸载不干净,导致 rustc 和 cargo 版本对不上,甚至出现 rustc 是旧版、cargo 是新版的情况。解决办法简单粗暴,卸载后删除 ~/.cargo 和 ~/.rustup,再重新安装即可。别担心删坏什么,这两个目录只是工具链和缓存,删掉重来反而比修补更快。

3. 亲手写第一个 Hello World:逐行拆解

3.1 最小程序:文件名、入口、缩进与分号

先把最小程序完整贴出来:

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

不用急着看输出,先看文件。文件名建议叫 main.rs 或 hello.rs,Rust 对文件名本身没有强制要求(除非你用 Cargo 的默认约定),但为了后面方便,统一用 main.rs 最省心。代码里第一行 fn main() 是入口,第二行是宏调用。缩进风格不是语法要求,但社区普遍用 4 空格。

分号值得专门说。println!("Hello, world!") 后面有个分号,分号表示这是一个"语句",执行完就完事,不产生值。如果把分号去掉,这一行就变成了整个 main 函数块的最后一个表达式,它的值会被当作函数的返回值,然后编译器就报错了。新手阶段分号写错的概率极高,我见过有人因为少一个分号排错半小时,所以一开始就养成数分号的习惯。

还有个细节:这里的缩进用的是 4 空格,不是 Tab。Rust 社区的 rustfmt 工具会自动格式化,但第 1 章你还在手写阶段,就按 4 空格来,后面跑 cargo fmt 也默认是这个规格。

3.2 把 "Hello, world!" 换成变量:let 绑定

第 1 章书上还没讲到变量,但我建议你直接改一行试试:

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

let greeting = "Hello, world!";是定义绑定的写法,Rust 里的变量默认不可变,翻译成人话就是:这个名字绑定了这个值,中途不让改。如果你想让值可变,得写成 let mut greeting。这个设计跟很多语言反着来,默认不可变,显式加 mut 才可变。初学者很容易顺手写 greeting = "other"; 然后被编译器教育,原因就是没加 mut。

println!("{}", greeting);里的 {} 是占位符,表示"把后面位置参数的值插进这里"。{} 对应的输出依赖值类型实现了 Display trait,字符串、数字都没问题。你要是放一个自定义结构体进去,就会触发我在第 4 节要讲的编译错误。

这里也顺便解释一下,Rust 的字符串字面量是 &str 类型,不是 String,两个类型在使用上差别很大,但 Hello World 阶段你只要知道"裸字符串基本都长这样"就够了。至于为什么这么设计,背后是内存所有权和借用的问题,第 4 章你会正式接触到。

3.3 从 println! 看格式化输出:{} 只是一切的开始

{} 是最简单的占位符,但 std::fmt 提供的远不止这些。往下推进一步,你可能会见到这些写法:

let name = "Rust"; let version = 1.84; println!("{name} {version:.2}"); println!("{:>10}", name); println!("{1} {0}", 42, "age");

第一行的命名参数能让代码可读性大大提升,{name} 会直接找同名变量。{version:.2} 表示浮点数保留 2 位小数。第二行的 {:>10} 是把内容右对齐占 10 个字符宽,CLI 输出对齐表格时非常有用。第三行的 {1} {0} 是位置参数,按顺序取后面参数,表达式总数和编号要对应上,否则编译期就给你脸色看。

这些格式化语法在官方文档里的名字叫 format spec(格式规范),完整规则包含占位符、位置、填充对齐、类型、精度几个维度。第 1 章不需要全背,但你要知道 println! 不是"只能打印固定一句话",它是一个可以承载结构化输出的工具。后面章节里关于格式化的内容,全都基于这里建立的直觉。

当你后面写大项目的时候,format! 宏会把这段经验延伸出新的用途:可以先把格式化后的字符串存到变量里,再决定是打印到控制台还是写到文件。println! 其实只是 format! 加一句输出到标准输出的糖。

4. 打印的进阶玩法:调试输出与常见坑

4.1 {:#?} 调试输出,结构体一打就现形

Hello World 只是起点,实际开发里你打印的更多是数组、结构体、HashMap。直接用 {} 会报错,这时候就要请出 Debug 派生的 {:#?}:

#[derive(Debug)] struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 10, y: 20 }; println!("{:?}", p); println!("{:#?}", p); }

{:?} 的输出会把结构体完整打出来,比如 Point { x: 10, y: 20 }。{:#?} 是 pretty-print 版本,多行缩进,嵌套结构一展开,字段层级一眼就能看清。调试代码的时候,我强烈推荐你先上 {:#?},比手写字段拼接省太多时间。

再展示一个常见场景:

let mut score = std::collections::HashMap::new(); score.insert("rust", 90); score.insert("python", 80); println!("{:#?}", score);

HashMap 这种内部顺序不固定的集合,用 {:#?} 打印出来是结构化的多行文本,调试时能看到每个键值对的 type 和值。Debug 输出虽然不是"面向用户"的漂亮格式,但它是你在性能排查、数据流观察阶段最常用的工具。

4.2 Display 未实现报错:新手最常见的编译错误

新手拿到结构体,第一反应就是 println!("{}", p);,然后编译器甩给你一段非常长的错误,开头长这样:the traitstd::fmt::Displayis not implemented forPoint。很多人看到 trait 就慌,其实这里表达的是:println! 想用 {} 打印 Point,但 Point 类型没有实现 Display,println! 不知道该怎么把它变成人话。

解决思路有两条。第一,如果只是调试用,把 {} 换成 {:?},并在结构体上加上 #[derive(Debug)],这是最快路径。第二,如果这个类型真的需要给用户看,就得手动实现 Display trait。我几乎每天用第一种,写 demo 也习惯性派生 Debug,Debug 加上后 {} 打不了还能随时换 {:?} 补救。

有时候报错还会伴随提示:perhaps two crates are both namedfmt,这种是命名冲突,通常是你给结构体命名跟 crate 或模块撞了。先按编译器提示改名字,多半就好了。

4.3 给自定义类型实现 Display 的正确姿势

手动实现 Display 也不难,格式是固定的:

use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Point({}, {})", self.x, self.y) } }

写完后 println!("{}", p) 就能输出 Point(10, 20)。注意 write! 这里也是宏,它返回 fmt::Result,正好匹配函数签名。实现 Display 的一个细节是:不要在 impl 里直接调用 println!,而是用 write! 把内容写进 Formatter,这样 println!、format!、eprintln! 都能复用这套实现。

到了这一步,你已经把 Hello World 玩成了一个小型接口设计练习:怎么让一个自定义类型跟标准格式化机制配合。等你以后写库,发现用户能用 {} 直接打印你的错误类型,体验会好很多。这也是 Rust 比其他语言更强调 trait 的原因之一——打印这种看似微不足道的操作,其实也有约定俗成的类型接口。

5. Rust 新手的三个典型问题与排查实录

5.1 中文输出乱码、注释乱码怎么办

很多同学控制台打印中文成了乱码。第一个检查点:源文件是不是 UTF-8 编码。Rust 源码默认按 UTF-8 处理,如果你用记事本把它存成了其他编码,编译器自己都会抱怨,更别提输出。第二个检查点:终端编码。部分终端默认代码页不一定和 UTF-8 匹配,切换到 UTF-8 再运行程序,基本能解决。如果你用的是 VS Code 的集成终端,把编码设为 UTF-8 即可。

乱码问题看着蠢,但实际排查过的人都知道有多费时间。我的习惯是:项目里所有 .rs 文件一律 UTF-8 无 BOM,终端能开 UTF-8 就开 UTF-8,一劳永逸。还有源码里写中文注释完全没问题,但别在字符串里硬编码需要特殊控制台才能显示的特殊符号,能省则省。

5.2 main 函数能不能有返回值

我见过不少人写 fn main() -> i32 { ... } 想模仿 C 语言的退出码。Rust 的稳定版本里,main 的返回值其实有讲究:可以返回 ()、Result<T, E> 或者满足 Termination trait 的类型。Hello World 阶段你写 fn main() -> i32 直接编译不通过,编译器会提示 main 的返回类型不合法。这不是语言不支持,而是要求你想清楚"退出状态"要从哪来。

更实用的场景是 main 返回 Result:

fn main() -> Result<(), Box<dyn std::error::Error>> { // 文件操作、网络请求等可能失败的逻辑 Ok(()) }

对,Hello World 阶段用不上,但你迟早会遇到。碰到 main 里带 ? 操作符的代码,就需要这个返回类型兜底。看到这里不用急着背,我提这个是想帮你建立心理准备:Rust 的错误处理从第一课就埋在下层,不要把 main 的签名当死规矩。

5.3 Cargo 构建慢、第一次构建卡住怎么办

用 Cargo 项目跑 Hello World,其实不需要任何第三方依赖,但第一次 cargo run 时它会做初始化,某些场景下会比较慢。最直接的建议是:先区分是"正常等待"还是"卡死"。Cargo 在下载依赖时终端会进进度条,等待几秒到几十秒都可能。如果你在项目里 import 了第三方 crate,并且依赖树比较复杂,构建时间会被拉长,和 Hello World 本体无关。

另一个常见坑是中途 Ctrl+C 把 Cargo 进程中断了,导致 Cargo 缓存处于"半写"状态,下次构建报莫名其妙的锁错误。解决办法是把 Cargo.lock 和 target 目录删掉重试,一般就能恢复。target 目录是构建产物和中间文件的存放处,删了不会伤及源码,放心删。

对了,还有个很实用的冷知识:Cargo 默认构建是 debug 模式,速度较快;追求运行性能要用 cargo build --release。第 1 章不需要关心 release,但你要知道调试和发布是两条流水线,这也是 Cargo 和裸 rustc 之间的另一个隐性差别。

6. 下一步:从 Hello World 走到真实项目

6.1 学完第 1 章的检查清单

这本书第 1 章篇幅很小,但学完你至少要确认自己这几件事都过关:

  • 会安装 rustup 并确认 rustc / cargo 可用
  • 会用 fn main 写一个最小的可执行程序
  • 区分 println! 与 print!,理解带 ! 的是宏
  • 会用 {} 占位符、命名参数和基本格式规范
  • 能把 println! 和 write! 的用法分清
  • 遇到 Display 未实现时,知道切 {:?} 或实现 Display

这六条全部做到,Hello World 这一章才算真正通关。不用急着往下一章冲,扎实的入口习惯比多刷十个例子都重要。我见过有人第一章跳得飞快,结果到写结构体的时候连 fn main 和宏都没分清,回头补课反而更慢。

6.2 从 Hello World 岔开的几条实战路线

第 1 章之外,整个 Rust 生态的入口其实很多。有人学 Rust 是为了 Tauri 桌面应用,Tauri 用 Rust 做后端壳子,配上前端界面,打包体积比 Electron 小太多,这条路线你需要补的是桌面端能力、文件系统、进程通信。有人是为了命令行工具,clap、anyhow 这些库都很成熟,用起来和打印 Hello World 一样直白,先写一个 hello 命令练手非常合适。

还有人被基于 Rust 的 AI Agent 框架吸引,最近 Rust 侧的 AI 生态发展很快,很多 agent 运行时和推理引擎的底层都是 Rust 写的,这条路线前置要求是异步、并发和序列化。更冷门一点的,有人在研究复古游戏平台的移植玩法,也有工业领域用 Rust 写 OPC UA 协议栈做设备互联的。这些方向听着各不相同,但底层都离不开你在这第 1 章打下的几个习惯:会建项目、会编译运行、会格式化输出、能读懂编译错误。

所以我一直觉得,Hello World 不是终点,它是一个小小的发令枪,把你推向 Cargo、trait、格式化宏、错误处理这些真正组成 Rust 日常的零件。先把手上的程序跑起来,再顺着你想去的方向走,哪条路都会比想象中顺。

我个人折腾 Rust 这么久的体会是,Hello World 这一章最值得记住的不是 println! 的输出结果,而是它逼你面对三件事:入口规矩、宏的存在、编译器的严格。这三样东西在后面的每一章都会被反复强化。你要是今天刚装好环境,可以把书里第 1 章的三个例子都跑一遍,顺便多加几个中文、数字、结构体的打印实验。跑通之后,再打开第 2 章的原生类型去。等你把基础类型刷完,回头再看这一章,你会惊讶于"就那么几行代码,原来藏了这么多讲究"。

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

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

立即咨询