Danh sách bài viết

Bài 298: println! vs eprintln! vs dbg!

Bài 298 của series Rust Cơ Bản — phân biệt rõ ba macro print cơ bản: println! ghi output thật của chương trình ra stdout (pipe-friendly), eprintln! ghi diagnostic / log ra stderr để không lẫn với output, và dbg!(expr) in cả file + line + biểu thức + giá trị rồi trả lại chính giá trị đó — chèn được vào giữa expression mà không phá flow. Đồng thời cảnh báo: dbg! chỉ dùng tạm, đừng commit lên production; production nên dùng log hoặc tracing crate sẽ học ở Bài 299 và Bài 301.

11/06/2026
9 phút đọc
0 lượt xem
1

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 | grep khô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ần io::stdout().flush() thủ công.
  • Dùng format! để build String không in, và biết khi nào nên đổi sang log / tracing crate.
2

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.

3

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.

4

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 = valuetrả 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)].

5

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 = value bằ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óa dbg!.
  • 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ùng log hoặc tracing crate.

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!().

6

print! / eprint! Và Vấn Đề Flush

Hai biến thể print!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.

7

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.

8

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.

9

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 = value ra stderr và trả lại value — chèn được giữa expression mà không phá flow.
  • dbg! debug-only — clippy lint clippy::dbg_macro chặn commit lên production.
  • print!/eprint! không thêm newline — cần io::stdout().flush() khi muốn output hiện ngay.
  • format! build String không in; format_args! low-level cho wrapper.
  • Production service nên dùng log (Bài 299) hoặc tracing (Bài 301) thay vì println! trực tiếp.
10

Bài Tập Củng Cố

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

  1. Bạn viết CLI tool numgen sinh 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?
  2. 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?
  3. Sau khi gọi print!("Nhập số: ") rồi read_line(), user không thấy prompt hiện ra. Nguyên nhân và cách sửa?
  4. 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?
  5. Trong code review, đồng nghiệp commit dbg!(payload) trong handler payment. Bạn comment phản đối — viết 2-3 lý do.
  6. Output của dbg!(x + 1) trên stderr trông như thế nào? Macro trả về giá trị gì?
Đáp án
  1. Số dùng println!("{n}") ghi stdout cho sort | uniq -c tiêu thụ. Progress dùng eprintln!("[progress] {done}/{total}") ghi stderr — hiện trên terminal nhưng không lẫn vào pipeline.
  2. Không compile. dbg!(v) consume v (move) rồi trả lại — nhưng vế phải không bind đi đâu nên giá trị bị drop, biến v không còn hợp lệ. Sửa: let v = dbg!(v); để rebind (hoặc dùng dbg!(&v) để dbg reference, không move).
  3. Stdout line-buffered: print! không có newline nên prompt nằm trong buffer, chưa flush ra terminal. read_line block chờ input → user thấy màn hình trống. Sửa: io::stdout().flush().unwrap(); ngay sau print!.
  4. format!("user-{user_id}-{year}") — trả về String trên heap, không in ra console. Đây chính là use case format! sinh ra để giải.
  5. (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ùng tracing::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.
  6. 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ủa x + 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ệ.
11

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.