Mục lục
- Mục Tiêu Bài Học
- println! — In Ra stdout
- eprintln! — In Ra stderr
- dbg! — Print Expression + File/Line, Trả Về Value
- dbg! Là Debug-Only — Đừng Commit Production
- print! / eprint! Và Vấn Đề Flush
- format! / format_args! — Build String Không In
- Khi Nào KHÔNG Dùng println/dbg
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Phân biệt rõ
println!(stdout) vàeprintln!(stderr) — biết khi nào dùng cái nào. - Tách output thật khỏi log: pipe
cargo run | grepkhông bị lẫn diagnostic. - Sử dụng
dbg!(expr)để in file:line + expression + value mà không phá flow code (vì macro trả lại chính value). - Biết
dbg!là debug-only — không commit lên production, có clippy lint chặn. - Hiểu
print!/eprint!không thêm newline và cầnio::stdout().flush()thủ công. - Dùng
format!để buildStringkhông in, và biết khi nào nên đổi sanglog/tracingcrate.
println! — In Ra stdout
Macro println! ghi ra standard output (stdout, file descriptor 1) kèm newline cuối dòng. Đây là macro quen thuộc từ "Hello, world!":
fn main() {
println!("Hello, world!");
let name = "An";
let age = 25;
println!("{name} chào, tuổi {age}"); // inline arg (Rust 2021+)
println!("Debug: {:?}", vec![1, 2, 3]); // {:?} = Debug trait
}
Format string của println! giống format!: {} gọi Display, {:?} gọi Debug, {:#?} pretty Debug, {name} capture biến cùng tên trong scope. Macro được expand tại compile time và type-check format string — viết sai số argument hoặc sai placeholder sẽ báo lỗi compile chứ không panic runtime.
Quy ước quan trọng: stdout dành cho output thật của chương trình — kết quả mà người dùng hoặc công cụ khác sẽ tiêu thụ qua pipe. Ví dụ ls | grep foo: ls viết tên file ra stdout để grep đọc qua pipe. Nếu chương trình của bạn trộn log debug vào stdout, mọi pipeline sẽ bị nhiễu.
eprintln! — In Ra stderr
Macro eprintln! hoạt động y hệt println! về cú pháp, chỉ khác đích đến: ghi ra standard error (stderr, file descriptor 2). Stderr được dành cho diagnostic, log, error message — những thứ con người cần đọc nhưng không phải output dùng cho pipe:
fn parse_line(line: &str) -> Option<i32> {
match line.trim().parse::<i32>() {
Ok(n) => Some(n),
Err(e) => {
eprintln!("[warn] không parse được {line:?}: {e}");
None
}
}
}
fn main() {
for line in ["10", "abc", "42"] {
if let Some(n) = parse_line(line) {
println!("{n}"); // output thật → stdout
}
}
}
Trong ví dụ trên, kết quả số đã parse đi ra stdout (cho pipeline tiêu thụ), còn warning về dòng lỗi đi ra stderr (cho người đọc terminal). Hai luồng tách biệt — pipe ./app | sort chỉ thấy số, warning vẫn hiện trên màn hình.
Use case: cargo run | grep ERROR tách stdout dữ liệu, log progress của Cargo (compile, finished) ghi ra stderr nên không lẫn vào kết quả grep. Mọi CLI tốt — ls, grep, jq, git — đều theo convention này.
dbg! — Print Expression + File/Line, Trả Về Value
Khi cần xem nhanh giá trị một biểu thức lúc debug, dbg! tiện hơn nhiều so với eprintln!("{}", x). Macro dbg!(expr) ghi ra stderr theo format [file:line] expr = value và trả lại chính giá trị — chèn được vào giữa expression mà không phá flow:
fn main() {
let x = 5;
let y = dbg!(x + 1) * 2; // in ra rồi nhân 2 luôn
dbg!(y);
let v = vec![1, 2, 3];
let sum: i32 = v.iter().map(|n| dbg!(n * 2)).sum();
println!("sum = {sum}");
}
Output trên stderr trông như sau:
[src/main.rs:3:13] x + 1 = 6
[src/main.rs:4:5] y = 12
[src/main.rs:7:30] n * 2 = 2
[src/main.rs:7:30] n * 2 = 4
[src/main.rs:7:30] n * 2 = 6
sum = 12
Để ý: dbg!(x + 1) * 2 in x + 1 = 6 nhưng biểu thức tiếp tục như thể không có dbg! — giá trị 6 được trả về rồi nhân 2 thành 12. Đây là điểm khác biệt then chốt với println!: bạn không phải kéo biểu thức ra biến tạm, không phải thêm dòng riêng, không phá pipeline iterator.
Gọi dbg!() không tham số in chỉ [file:line] — như "tôi đã chạy đến đây". Gọi dbg!(a, b, c) với nhiều argument trả về tuple (a, b, c). Format value qua {:#?} (pretty Debug) nên type cần impl Debug — nếu chưa, derive thêm #[derive(Debug)].
dbg! Là Debug-Only — Đừng Commit Production
dbg! được Rust team thiết kế chỉ dùng tạm khi đang gỡ lỗi, không phải để log chính thức. Có ba lý do:
- Output noisy. Format
[file:line] expr = valuebằng pretty Debug rất rườm rà — không phù hợp log production cần parse được hoặc structured. - Move ownership.
dbg!(some_string)consume biến (vì macro nhận by value rồi trả lại). Quên là gặp lỗi compile "value moved" sau khi xóadbg!. - Không filter được.
println!/eprintln!/dbg!luôn chạy — không có level, không có module filter. Để bật/tắt log ở production phải dùngloghoặctracingcrate.
Anti-pattern thường gặp:
// XẤU — commit lên production
pub fn process_order(order: Order) -> Result<Receipt, Error> {
let validated = dbg!(validate(&order))?;
let total = dbg!(calculate_total(&validated));
dbg!(save_to_db(&validated, total))
}
Clippy có lint clippy::dbg_macro cảnh báo mọi lần dùng dbg! — bật trong Cargo.toml hoặc clippy.toml, hoặc thêm #![deny(clippy::dbg_macro)] ở crate root để CI fail nếu lỡ commit. Rule of thumb: dùng dbg! trong terminal khi đang trace bug → tìm ra nguyên nhân → xóa tất cả dbg! trước khi commit. Nếu thực sự cần log dài hạn, đổi sang log::debug!() hoặc tracing::debug!().
print! / eprint! Và Vấn Đề Flush
Hai biến thể print! và eprint! giống println! / eprintln! nhưng không tự thêm newline. Dùng khi muốn in nối tiếp trên cùng dòng, vd progress bar hoặc prompt input:
use std::io::{self, Write};
fn main() {
print!("Nhập tên bạn: ");
io::stdout().flush().unwrap(); // BẮT BUỘC
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap();
println!("Chào {}!", name.trim());
}
Điểm cần nhớ: stdout là line-buffered khi gắn terminal — output chỉ flush khi gặp \n hoặc khi buffer đầy. Vì print! không có newline, prompt "Nhập tên bạn: " không hiện trước khi read_line block chờ input — user thấy màn hình trống và bối rối. Phải gọi io::stdout().flush() để ép buffer xuống terminal trước khi đọc input.
Với eprint!, stderr thường unbuffered (không buffer) nên flush không cần — nhưng quy ước vẫn nên flush khi cần ngay để code portable. println!/eprintln! không gặp vấn đề này vì newline tự trigger flush ở line-buffered stream.
format! / format_args! — Build String Không In
Khi cần kết quả là String (để lưu, gửi network, log qua crate khác) thay vì in ra console, dùng format! — cú pháp y hệt nhưng trả về String mới allocate trên heap:
let name = "An";
let age = 25;
let msg: String = format!("{name} tuổi {age}");
// Dùng cho log crate:
log::info!("user {} login", msg);
// Dùng làm key HashMap, return value, etc.
let key = format!("user:{}", user_id);
Có macro thấp tầng hơn nữa: format_args! — trả về fmt::Arguments không allocate, dùng khi viết wrapper print/log để chuyển tiếp format string sang write! hoặc các sink khác (vd writeln!(file, ...), write!(&mut buffer, ...)). Người dùng cuối hiếm khi gọi trực tiếp format_args!, nhưng biết nó tồn tại để hiểu vì sao mọi print macro Rust đều thống nhất format string syntax: chúng đều build trên format_args! bên dưới.
Cheat sheet 5 macro liên quan: print!/println! in stdout; eprint!/eprintln! in stderr; write!/writeln! ghi vào bất kỳ Write impl (file, buffer, network); format! build String; format_args! low-level cho wrapper.
Khi Nào KHÔNG Dùng println/dbg
Print macro stdlib đủ cho CLI nhỏ, script, prototype. Nhưng khi vào production service cần:
- Log level (trace/debug/info/warn/error) để filter theo môi trường.
- Module filter bật log chi tiết chỉ ở module đang nghi vấn.
- Structured logging field key=value cho cho công cụ phân tích (Elastic, Loki).
- Span / context theo request id qua nhiều async function.
- Format JSON cho container log.
...thì println!/eprintln!/dbg! không đáp ứng. Group 37 này bạn sẽ học hai crate chuẩn ngành: Bài 299: log crate (facade với info!/warn!/error! macro + backend rời như env_logger ở Bài 300), và Bài 301: tracing crate (structured + span, chuẩn cho mọi service async/web hiện đại). Khi đã quen log/tracing, gần như mọi println! trong source code production sẽ biến thành info! / debug!, và dbg! chỉ xuất hiện tạm khi đang trace bug rồi xóa ngay.
Tổng Kết
println!ghi ra stdout, dành cho output thật của chương trình — pipe-friendly.eprintln!ghi ra stderr, dành cho log / diagnostic / error — không lẫn với pipeline.dbg!(expr)ghi[file:line] expr = valuera stderr và trả lại value — chèn được giữa expression mà không phá flow.dbg!debug-only — clippy lintclippy::dbg_macrochặn commit lên production.print!/eprint!không thêm newline — cầnio::stdout().flush()khi muốn output hiện ngay.format!buildStringkhông in;format_args!low-level cho wrapper.- Production service nên dùng
log(Bài 299) hoặctracing(Bài 301) thay vìprintln!trực tiếp.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Bạn viết CLI tool
numgensinh ra danh sách số rồi dùng./numgen | sort | uniq -c. Tool cũng cần in progress "đã sinh 1000/10000". Mỗi luồng đi qua macro nào để pipeline vẫn đúng? let v = vec![1, 2, 3]; dbg!(v);rồi sau đóprintln!("{:?}", v);— đoạn này compile không? Nếu lỗi, sửa thế nào?- Sau khi gọi
print!("Nhập số: ")rồiread_line(), user không thấy prompt hiện ra. Nguyên nhân và cách sửa? - Bạn cần build chuỗi
"user-42-2026"để dùng làm key cache, không cần in ra console. Macro nào phù hợp? - Trong code review, đồng nghiệp commit
dbg!(payload)trong handler payment. Bạn comment phản đối — viết 2-3 lý do. - Output của
dbg!(x + 1)trên stderr trông như thế nào? Macro trả về giá trị gì?
Đáp án
- Số dùng
println!("{n}")ghi stdout chosort | uniq -ctiêu thụ. Progress dùngeprintln!("[progress] {done}/{total}")ghi stderr — hiện trên terminal nhưng không lẫn vào pipeline. - Không compile.
dbg!(v)consumev(move) rồi trả lại — nhưng vế phải không bind đi đâu nên giá trị bị drop, biếnvkhông còn hợp lệ. Sửa:let v = dbg!(v);để rebind (hoặc dùngdbg!(&v)để dbg reference, không move). - Stdout line-buffered:
print!không có newline nên prompt nằm trong buffer, chưa flush ra terminal.read_lineblock chờ input → user thấy màn hình trống. Sửa:io::stdout().flush().unwrap();ngay sauprint!. format!("user-{user_id}-{year}")— trả vềStringtrên heap, không in ra console. Đây chính là use case format! sinh ra để giải.- (a) Output noisy không phù hợp log production, ghi pretty Debug có thể leak thông tin nhạy cảm như card number ra log. (b)
dbg!luôn chạy, không có log level để filter trong production. (c) Nếu cần log dài hạn, phải dùngtracing::info!với structured field — vừa control được level, vừa filter được PII, vừa đúng format cho công cụ phân tích. - Format trên stderr:
[src/main.rs:3:13] x + 1 = 6(đường dẫn file, line, column, biểu thức gốc, value qua pretty Debug). Macro trả về chính giá trị củax + 1(ở đây là6), nên có thể chèn giữa expression:let y = dbg!(x + 1) * 2;hoàn toàn hợp lệ.
Bài Tiếp Theo
Bài 299: log Crate — Logging Facade — crate log cung cấp trait Log và macro info!/warn!/error!/debug!/trace! chuẩn cho cả ecosystem Rust, nhưng không tự in — cần backend riêng (env_logger, simplelog, tracing-log). Bài sau sẽ làm rõ vai trò "facade" và cách dùng level filter để bật/tắt log theo môi trường.
