Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Tạo được một project Rust mới qua
cargo newvà hiểu Cargo tự sinh ra những file gì. - Đọc hiểu file
src/main.rsmặc định:fn main,println!macro, semicolon kết thúc statement. - Quan sát được log của Cargo khi
cargo run(Compiling → Finished → Running) và biết binary nằm ở đâu. - Sửa code để đọc input từ
stdinbằngstd::io::stdin().read_line(). - Đổi qua đọc input từ command-line argument bằng
std::env::args(). - Build release ra binary đứng độc lập (~400 KB) và copy đi máy khác chạy không cần Rust toolchain.
- Thêm dependency thứ ba qua
cargo add coloredvà in chữ có màu ra terminal. - Nắm lại tổng quan 8 bài đầu của Nhóm 3 — Cargo cơ bản — trước khi sang Nhóm 4.
Bài 24 là bài thực hành tổng hợp đóng lại Nhóm 3. Tất cả khái niệm dùng trong bài (cargo new, Cargo.toml, build vs run, profile dev/release, Cargo.lock) đều đã học ở các bài B17 đến B23.
Tạo Project Mới Với cargo new
Mở terminal, chuyển tới thư mục bạn hay để code (ví dụ ~/code), rồi gõ:
cargo new hello
cd hello
ls -la
Output mong đợi (macOS / Linux):
Creating binary (application) `hello` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
drwxr-xr-x 6 user staff 192 Jun 9 10:00 .
drwxr-xr-x 10 user staff 320 Jun 9 10:00 ..
drwxr-xr-x 10 user staff 320 Jun 9 10:00 .git
-rw-r--r-- 1 user staff 8 Jun 9 10:00 .gitignore
-rw-r--r-- 1 user staff 78 Jun 9 10:00 Cargo.toml
drwxr-xr-x 3 user staff 96 Jun 9 10:00 src
Cargo vừa làm 4 việc:
- Tạo folder
hello/cùng tên package. - Sinh file manifest
Cargo.tomlvới thông tin tối thiểu. - Sinh file entry-point
src/main.rschứa Hello World. - Khởi tạo git repo (
.git) và viết sẵn.gitignorebỏ quatarget/.
Nếu không muốn Cargo init git (đã có repo cha bao quanh chẳng hạn), thêm flag --vcs none:
cargo new hello --vcs none
Mặc định edition là 2024 ở Cargo ≥ 1.83 (rust toolchain 2026). Muốn ép edition cũ thì cargo new hello --edition 2021.
Đọc File Mặc Định: main.rs & Cargo.toml
Mở src/main.rs:
cat src/main.rs
fn main() {
println!("Hello, world!");
}
Ba dòng này gói gọn nhiều khái niệm cốt lõi:
fn: keyword khai báo function.main: tên function đặc biệt — entry point của binary crate. Khi chạy executable, OS gọimainđầu tiên. Tên này bắt buộc, không thay được.(): danh sách parameter rỗng.mainkhông nhận tham số trực tiếp (muốn đọc CLI args thì dùngstd::env::args()— sẽ học ở Bước 6).{ ... }: function body, một block expression.println!: macro chứ không phải function — nhận biết qua dấu!. Macro được expand ở compile time, cho phép format string với số lượng argument bất kỳ. Đây là điểm khác với function (cố định số argument)."Hello, world!": string literal kiểu&'static str— embed vào binary section read-only.;: semicolon kết thúc một statement. Thiếu nó thì dòng đó thành expression và sẽ trả value — gây lỗi compile vìprintln!trả()nhưng dòng cuối function void thì chấp nhận được. Sẽ học sâu hơn ở B46: Expression vs Statement.
Tiếp tục mở Cargo.toml:
cat Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
Đúng như đã học ở Bài 20: [package] với 3 field tối thiểu, [dependencies] trống. Cargo chưa thêm authors, license, description — đó là tuỳ chọn khi publish lên crates.io.
Chạy Lần Đầu Với cargo run
Trong thư mục hello/, gõ:
cargo run
Output:
Compiling hello v0.1.0 (/Users/you/code/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Hello, world!
3 dòng log Cargo cần để ý:
- Compiling hello v0.1.0: Cargo gọi
rustccompile cratehello, version từ Cargo.toml. - Finished `dev` profile [unoptimized + debuginfo]: dùng profile
dev(đã học ở Bài 22). Build nhanh, không optimize, có debug symbol — đúng cho dev workflow. - Running `target/debug/hello`: Cargo tự thực thi binary đã build.
Kiểm tra file thực tế:
ls -lh target/debug/hello
-rwxr-xr-x 1 user staff 3.6M Jun 9 10:01 target/debug/hello
Binary dev khá to (~3-4 MB) vì còn debug info. Đến Bước 7 build release sẽ thấy giảm xuống ~400 KB. Có thể chạy trực tiếp không cần Cargo:
./target/debug/hello
# Hello, world!
Folder target/ là build cache — Cargo đã ghi vào .gitignore nên không cần commit.
Modify Code — Đọc Tên Từ stdin
Hello world tĩnh thì nhàm. Đổi sang hỏi tên user rồi chào. Mở src/main.rs sửa thành:
use std::io::{self, BufRead, Write};
fn main() {
print!("Tên bạn là gì? ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().lock().read_line(&mut input).unwrap();
let name = input.trim();
println!("Chào {name}, từ Rust!");
}
Chạy:
cargo run
# Tên bạn là gì? Canh
# Chào Canh, từ Rust!
Giải thích từng phần mới xuất hiện:
use std::io::{self, BufRead, Write};: import moduleiovà 2 traitBufRead(choread_line),Write(choflush). Trait phải in scope thì method trait đó mới gọi được.print!: macro tương tựprintln!nhưng không xuống dòng. Vì vậy phảiflushstdout — mặc định stdout buffer theo dòng, không flush thì user không thấy prompt.let mut input = String::new(): tạo mộtStringrỗng, growable, trên heap. Phảimutvìread_linesẽ append vào nó.letsẽ học sâu ở B25,mutở B26.io::stdin().lock().read_line(&mut input): lấy handle stdin, lock để truy cập exclusive, đọc tới ký tự\nđầu tiên rồi append vàoinput. Truyền&mut input— mutable reference, vì hàm cần modify chứ không lấy ownership (sẽ học ở Nhóm 9-10)..unwrap():read_linetrảResult<usize, io::Error>.unwrap()tạm thời: lấyOkhoặc panic nếuErr. Sẽ học cách xử lý đúng (?operator) ở Nhóm 19 — Error Handling..trim(): cắt whitespace đầu/cuối — quan trọng vìread_linegiữ lại ký tự\nmà user nhấn Enter. Không trim thì sẽ in ra"Chào Canh\n, từ Rust!"— Enter rơi giữa câu.println!("Chào {name}, từ Rust!"): cú pháp captured identifier — placeholder{name}tự bind vào biến cùng tên trong scope. Đây là feature từ Rust 2021+ — gọn hơnprintln!("Chào {}, ...", name).
Pitfall: nếu thay print! thành println! thì flush không bắt buộc (xuống dòng auto-flush), nhưng prompt sẽ ở dòng riêng — kém UX.
Modify Code — Đọc Tên Từ Command Line
Cách khác: thay vì hỏi interactive, nhận tên qua command-line argument. Sửa src/main.rs thành:
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let name = args.get(1).map(|s| s.as_str()).unwrap_or("world");
println!("Hello, {name}!");
}
Chạy với argument:
cargo run -- Canh
# Hello, Canh!
cargo run
# Hello, world!
Chú ý dấu --: phân tách argument cho Cargo và argument cho binary. Không có -- thì Cargo tưởng Canh là flag của nó và báo lỗi.
Giải thích:
env::args(): trả vềArgs— iterator các argument dạngString. Phần tử[0]luôn là path của binary (ví dụtarget/debug/hello),[1]trở đi là argument do user truyền..collect(): tiêu thụ iterator, gom vàoVec<String>. Type annotationlet args: Vec<String>giúpcollectbiết collect vào kiểu nào.args.get(1): lấy phần tử index 1 dạngOption<&String>— an toàn (trảNonenếu thiếu argument), khácargs[1]sẽ panic out-of-bounds.Optionsẽ học ở B92..map(|s| s.as_str()): nếu có giá trị, convert&Stringsang&strđể khớp với"world"ởunwrap_or..unwrap_or("world"): nếu làSome(x)trảx, nếuNonetrả"world". Pattern phổ biến để có default value.
Trong thực tế, parse CLI phức tạp (subcommand, flag, validation) thì dùng crate clap. Ở đây chỉ minh hoạ stdlib.
Compile Release Build
Mặc định cargo run dùng profile dev — binary có debug info, opt-level 0, kích thước 3-4 MB. Production cần profile release:
cargo build --release
ls -lh target/release/hello
./target/release/hello
Output:
Compiling hello v0.1.0 (/Users/you/code/hello)
Finished `release` profile [optimized] target(s) in 1.8s
-rwxr-xr-x 1 user staff 412K Jun 9 10:05 target/release/hello
Hello, world!
Quan sát:
- Binary giảm từ ~3.6 MB (dev) xuống ~400 KB (release). Có thể giảm thêm bằng
strip = true+lto = "thin"trong[profile.release](đã học ở B22). - Build time lâu hơn (~1.8s vs 0.4s) vì optimizer phải làm việc nhiều.
- Path đổi từ
target/debug/sangtarget/release/— hai cache độc lập.
Binary release là statically linked (trừ libc system). Có thể copy đi máy khác cùng OS/arch chạy được luôn — không cần Rust toolchain, không cần Cargo, không cần internet:
scp target/release/hello user@server:/usr/local/bin/
ssh user@server hello
Đây là lý do nhiều CLI tool nổi tiếng (ripgrep, fd, bat, tokei) viết bằng Rust: distribute chỉ 1 file binary duy nhất, không có dependency runtime.
Lưu ý cross-platform: binary build trên macOS arm64 chỉ chạy trên macOS arm64. Cross-compile sang Linux x64 cần thêm rustup target add x86_64-unknown-linux-gnu và linker phù hợp — sẽ đề cập ở Nhóm Cargo Nâng Cao.
Add Dependency — colored Crate
Để minh hoạ workflow add dependency, dùng crate colored — in chữ có màu ANSI ra terminal. Trong thư mục hello/:
cargo add colored
Output:
Updating crates.io index
Adding colored v3.0.0 to dependencies
Features:
- no-color
Updating crates.io index
Locking 2 packages to latest compatible versions
Cargo đã làm 3 việc:
- Cập nhật
Cargo.tomlthêm dòngcolored = "3"vào[dependencies]. - Resolve version graph và ghi vào
Cargo.lock(đã học ở B23). - Tải source crate về cache
~/.cargo/registry/.
Mở lại Cargo.toml:
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
colored = "3"
Sửa src/main.rs dùng colored:
use colored::*;
fn main() {
println!("{}", "Hello, world!".green().bold());
println!("{}", "Cảnh báo từ Rust".yellow().italic());
println!("{}", "Lỗi!".red().underline());
}
Chạy:
cargo run
Lần này Cargo phải compile thêm colored và các transitive dep:
Compiling colored v3.0.0
Compiling hello v0.1.0 (/Users/you/code/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.3s
Running `target/debug/hello`
Hello, world! (xanh lá, in đậm)
Cảnh báo từ Rust (vàng, nghiêng)
Lỗi! (đỏ, gạch chân)
Lần build tiếp theo sẽ nhanh hơn vì colored đã ở build cache.
Giải thích code:
use colored::*;: glob import — kéo toàn bộ public item của cratecoloredvào scope, gồm traitColorize(thêm method.green(),.bold()... cho mọi&strvàString)..green().bold(): method chain — mỗi method trảColoredStringnên gọi liên tiếp được.- Output có màu khi terminal hỗ trợ ANSI (mọi terminal hiện đại đều có). Pipe vào file thì ANSI escape bị strip nếu colored detect được — không bẩn output.
Đây là chính xác workflow bạn sẽ làm hằng ngày: cargo add thêm crate, use import, viết code dùng. Crates.io có hơn 150.000 crate — bất kỳ vấn đề thông dụng nào đã có sẵn.
Tổng Kết Nhóm 3 — Cargo Cơ Bản
Đã xong 8 bài Nhóm 3. Recap lại để dắt qua Nhóm 4:
- B17 — Cargo là gì: build system + package manager + test runner + doc generator, all-in-one. Tương đương npm + tsc + jest + typedoc gộp lại.
- B18 — cargo new vs cargo init:
newtạo folder mới,initáp dụng vào folder hiện tại; flag--lib,--vcs,--edition. - B19 — Cấu trúc folder:
Cargo.toml,Cargo.lock,src/main.rshoặcsrc/lib.rs,src/bin/,tests/,benches/,examples/,target/. - B20 — Cargo.toml anatomy:
[package],[dependencies]3 dạng (shorthand, table, git, path),[dev-dependencies],[features],[[bin]],[profile.*]. - B21 — build vs run vs check:
checktypecheck siêu nhanh (không gen binary),buildcompile + link,run= build + execute. - B22 — Profile dev vs release: dev opt-level 0 + debug info (build nhanh), release opt-level 3 + LTO (binary nhỏ và nhanh nhất).
- B23 — Cargo.lock: ghi exact version graph; binary crate nên commit, library crate không commit.
- B24 — Hello World với Cargo (bài này): thực hành toàn bộ flow từ
cargo new→cargo run→cargo build --release→cargo add.
Đến đây bạn đã có nền vững về Cargo đủ để build, chạy, distribute bất kỳ project Rust nào. Các nhóm tiếp theo tập trung vào ngôn ngữ Rust — bắt đầu bằng cách Rust quản lý dữ liệu qua biến và mutability.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Sau
cargo new hello, Cargo tạo ra những file và folder gì? Trong đó cái nào bạn nên / không nên commit lên git? - Trong
println!("Hello, world!");, vì saoprintlncó dấu!? Khác gì với function thường? - Bạn viết
print!("Nhập gì đó: ")rồiread_linenhưng prompt không hiện ra trước, mãi tới khi user gõ xong mới thấy. Vì sao và sửa thế nào? - Chạy
cargo run Canhbáo lỗierror: unexpected argument 'Canh'. Lệnh đúng phải gõ sao để binary nhậnCanhlàm argument? - So sánh kích thước binary của
target/debug/hellovàtarget/release/hello. Vì sao có sự khác biệt lớn? Liệt kê thêm 2 cách giảm kích thước binary release hơn nữa.
Đáp án
- Cargo tạo:
Cargo.toml(commit),src/main.rs(commit),.gitignore(commit),.git/(chính là git repo). Sau lần build đầu sẽ thêmCargo.lock(commit nếu là binary crate, không commit nếu library) vàtarget/(KHÔNG commit — đã có sẵn trong.gitignore). - Dấu
!báo hiệu đây là macro chứ không phải function. Macro được expand ở compile time, cho phép nhận số argument không cố định và format string với các placeholder{}. Function thường có signature cố định, không thể nhận variadic argument an toàn như macro. - Vì stdout buffer theo dòng —
print!không có\nnên data ở trong buffer, chưa flush ra terminal. Sửa: thêmio::stdout().flush().unwrap();ngay sauprint!. Hoặc đổi sangprintln!(auto-flush nhưng prompt sẽ ở dòng riêng). cargo run -- Canh. Dấu--phân tách argument cho Cargo (bên trái) và argument cho binary chạy (bên phải). Không có--Cargo sẽ parseCanhnhư flag của chính nó.- Dev ~3-4 MB vì có đầy đủ debug symbol (DWARF info) và opt-level 0; release ~400 KB vì opt-level 3, không debug info. Để giảm thêm: (a)
strip = truebỏ symbol còn sót, (b)lto = "thin"hoặc"fat"link-time optimization, (c)codegen-units = 1cho LTO hiệu quả tối đa, (d)panic = "abort"bỏ unwind code, (e)opt-level = "z"ưu tiên size hơn speed. Tất cả đặt trong[profile.release]củaCargo.toml.
Bài Tiếp Theo
Hết Nhóm 3 — Cargo cơ bản. Sang Nhóm 4 tạm dừng tooling và đi vào ngôn ngữ Rust: cách khai báo biến, immutable vs mutable, shadowing, hằng số.
Bài 25: let — Binding Cơ Bản Trong Rust — cú pháp let x = 5;, vì sao Rust mặc định immutable, type inference vs annotation rõ ràng, prefix _ để silence unused warning, và so sánh với const/let của JavaScript hay = của Python để bạn dễ chuyển ngữ cảnh.
