Danh sách bài viết

Bài 24: Hello World Với Cargo — Code, Build, Run

Bài 24 của series Rust Cơ Bản — bài cuối Nhóm 3 (Cargo cơ bản). Thực hành đầy đủ một chương trình Hello World từ cargo new, đọc src/main.rs mặc định và giải thích từng dòng (fn main, println! macro, semicolon), chạy cargo run quan sát log Cargo, sửa code đọc tên user từ stdin rồi từ env::args, build release ra binary standalone trong target/release/, cuối cùng cargo add colored để output có màu. Đóng lại bằng tổng kết 8 bài Nhóm 3 trước khi sang Nhóm 4 — Variables & Mutability.

09/06/2026
12 phút đọc
2 lượt xem
1

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 new và hiểu Cargo tự sinh ra những file gì.
  • Đọc hiểu file src/main.rs mặ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ừ stdin bằng std::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 colored và 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.

2

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.toml với thông tin tối thiểu.
  • Sinh file entry-point src/main.rs chứa Hello World.
  • Khởi tạo git repo (.git) và viết sẵn .gitignore bỏ qua target/.

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.

3

Đọ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ọi main đầu tiên. Tên này bắt buộc, không thay được.
  • (): danh sách parameter rỗng. main không nhận tham số trực tiếp (muốn đọc CLI args thì dùng std::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.

4

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 rustc compile crate hello, 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.

5

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 module io và 2 trait BufRead (cho read_line), Write (cho flush). 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ải flush stdout — 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ột String rỗng, growable, trên heap. Phải mutread_line sẽ append vào nó. let sẽ học sâu ở B25, mutB26.
  • 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ào input. 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_line trả Result<usize, io::Error>. unwrap() tạm thời: lấy Ok hoặc panic nếu Err. 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_line giữ lại ký tự \n mà 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ơn println!("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.

6

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 và báo lỗi.

Giải thích:

  • env::args(): trả về Args — iterator các argument dạng String. 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ào Vec<String>. Type annotation let args: Vec<String> giúp collect biết collect vào kiểu nào.
  • args.get(1): lấy phần tử index 1 dạng Option<&String> — an toàn (trả None nếu thiếu argument), khác args[1] sẽ panic out-of-bounds. Option sẽ học ở B92.
  • .map(|s| s.as_str()): nếu có giá trị, convert &String sang &str để khớp với "world"unwrap_or.
  • .unwrap_or("world"): nếu là Some(x) trả x, nếu None trả "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.

7

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/ sang target/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.

8

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.toml thêm dòng colored = "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 crate colored vào scope, gồm trait Colorize (thêm method .green(), .bold()... cho mọi &strString).
  • .green().bold(): method chain — mỗi method trả ColoredString nê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.

9

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: new tạ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.rs hoặc src/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: check typecheck siêu nhanh (không gen binary), build compile + 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 newcargo runcargo build --releasecargo 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.

10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 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?
  2. Trong println!("Hello, world!");, vì sao println có dấu !? Khác gì với function thường?
  3. Bạn viết print!("Nhập gì đó: ") rồi read_line như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?
  4. Chạy cargo run Canh báo lỗi error: unexpected argument 'Canh'. Lệnh đúng phải gõ sao để binary nhận Canh làm argument?
  5. So sánh kích thước binary của target/debug/hellotarget/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
  1. 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êm Cargo.lock (commit nếu là binary crate, không commit nếu library) và target/ (KHÔNG commit — đã có sẵn trong .gitignore).
  2. 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.
  3. Vì stdout buffer theo dòng — print! không có \n nên data ở trong buffer, chưa flush ra terminal. Sửa: thêm io::stdout().flush().unwrap(); ngay sau print!. Hoặc đổi sang println! (auto-flush nhưng prompt sẽ ở dòng riêng).
  4. 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ẽ parse Canh như flag của chính nó.
  5. 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 = true bỏ symbol còn sót, (b) lto = "thin" hoặc "fat" link-time optimization, (c) codegen-units = 1 cho 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ủa Cargo.toml.
11

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.