Danh sách bài viết

Bài 113: pub Keyword — Visibility

Bài 113 của series Rust Cơ Bản — hệ thống visibility với từ khoá pub và các biến thể granular. Khác với nhiều ngôn ngữ mặc định public, Rust chọn mặc định private - mọi item chỉ thấy được trong module khai báo và các child module. Muốn cho ngoài thấy, phải explicit thêm pub. Bài này đi qua: default private và lý do về encapsulation, pub cơ bản cho fn/struct/enum, lưu ý struct pub không tự pub field, enum pub thì variant tự pub theo, ba biến thể granular pub(crate)/pub(super)/pub(in path) cho internal API, và preview pub use để re-export - học chi tiết ở Bài 116.

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

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu vì sao Rust chọn default private cho mọi item và lợi ích về encapsulation mà lựa chọn này mang lại.
  • Dùng được pub để expose fn, struct, enum ra khỏi module — cú pháp đặt trước item.
  • Phân biệt rõ struct pub không đồng nghĩa field pub: muốn truy cập field từ ngoài phải thêm pub riêng cho từng field.
  • Biết variant của pub enum tự động pub theo enum — không cần ghi pub trước từng variant.
  • Dùng được ba biến thể granular: pub(crate) cho item internal nội bộ crate, pub(super) cho parent module, pub(in path) cho một path cụ thể.
  • Hiểu pub use foo::bar; ở mức preview — pattern re-export để flatten public API. Chi tiết và facade pattern sẽ ở Bài 116.
  • Áp dụng các quy tắc trên để thiết kế một internal API có ranh giới rõ ràng — public cho user, private/internal cho team.
2

Default Private — Encapsulation

Rust quy định: mặc định mọi item là private. Một item private chỉ visible trong:

  • Chính module khai báo nó.
  • Mọi child module (module con) nằm bên trong module đó.

Sibling module và parent module không thấy item private. Khác với Java (mặc định package-private), Python (không có khái niệm private thật, chỉ convention _name), hay JavaScript (mọi export đều public), Rust ép bạn phải nghĩ về ranh giới ngay từ đầu — viết pub là một quyết định có ý thức.

// src/lib.rs
mod auth {
    // Item này default private - chỉ visible trong module `auth` và child của `auth`
    fn hash_password(raw: &str) -> String {
        format!("hashed:{raw}")
    }

    // Hàm cùng module - gọi được hash_password vì cùng scope private
    pub fn login(user: &str, pw: &str) -> bool {
        let _h = hash_password(pw);
        user == "admin"
    }
}

fn main() {
    // OK: login đã pub
    let _ = auth::login("admin", "123");

    // Compile error: hash_password là private của module auth
    // let _ = auth::hash_password("123");
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // error[E0603]: function `hash_password` is private
}

Triết lý đằng sau: encapsulation by default. Khi viết một module, mọi helper, mọi cấu trúc nội bộ đều được giấu khỏi thế giới bên ngoài cho đến khi bạn chủ động expose. Điều này có ba lợi ích lớn:

  • Tự do refactor: thay đổi tên, signature, hoặc xoá item private không bao giờ ảnh hưởng caller bên ngoài — vì không ai bên ngoài thấy nó.
  • Bảo toàn invariant: nếu một struct User có ràng buộc "tuổi không âm", che field age sau private và chỉ expose constructor đảm bảo điều kiện sẽ không ai bên ngoài phá được.
  • API surface gọn: cargo doc chỉ liệt kê item pub — user chỉ thấy đúng những gì bạn muốn họ thấy, không bị nhiễu bởi helper nội bộ.

Lưu ý quan trọng về quan hệ parent-child: child thấy item private của parent (qua super::), nhưng parent không thấy item private của child. Đây là quy tắc bất đối xứng đặc trưng của Rust — sẽ rõ hơn khi bạn dùng pub(super) ở mục 7.

3

pub Cơ Bản — fn / struct / enum

Để expose một item ra ngoài module khai báo, đặt pub trước item. Quy tắc áp dụng đồng nhất cho fn, struct, enum, const, static, type, trait, mod (cả module cũng default private).

mod api {
    // Hàm public - mọi nơi đều gọi được api::process
    pub fn process(input: &str) -> String {
        normalize(input)
    }

    // Helper vẫn private - không lộ ra ngoài
    fn normalize(s: &str) -> String {
        s.trim().to_lowercase()
    }

    // Struct public - tên type lộ ra ngoài, nhưng field thì chưa (xem mục 4)
    pub struct User {
        pub name: String,
        age: u32, // field này vẫn private mặc dù struct là pub
    }

    // Enum public - mọi variant tự động pub theo enum
    pub enum Status {
        Ok,
        NotFound,
        InternalError,
    }
}

fn main() {
    let _ = api::process("  Hello  ");
    let _ = api::Status::Ok;
}

Khi pub mod api; hay pub fn process(), item đó được expose ra tới chỗ module cha cho phép. Nói cách khác, để dùng api::process từ ngoài, cả module api phải pub process phải pub. Hai điều kiện tách rời nhau: một item pub bên trong module private vẫn vô hình ra ngoài. Nắm chắc nguyên tắc "visible đến đâu tuỳ chuỗi pub liền nhau" sẽ tránh nhiều rắc rối khi cấu trúc lại module tree.

Quy ước thực hành: chỉ pub những gì bạn chắc chắn muốn đưa thành public API. Một item pub rồi rất khó rút lại — vì có thể đã có code khác phụ thuộc, rút lại sẽ là breaking change.

4

Struct Field — Cần pub Từng Field

Đây là chỗ rất nhiều người mới Rust nhầm. Đặt pub trước struct chỉ expose tên type, không expose field. Mỗi field muốn truy cập từ ngoài phải tự khai báo pub.

mod model {
    pub struct User {
        pub name: String, // field name public
        age: u32,         // field age vẫn private
    }

    impl User {
        // Constructor public - ép caller đi qua đây thay vì tự dựng struct literal
        pub fn new(name: String, age: u32) -> Self {
            User { name, age }
        }

        pub fn age(&self) -> u32 {
            self.age
        }
    }
}

fn main() {
    // OK: đi qua constructor
    let mut u = model::User::new("Canh".into(), 30);

    // OK: name là pub field
    u.name = "Canh NV".into();
    println!("{}", u.name);

    // OK: getter
    println!("{}", u.age());

    // Compile error: age là private field
    // u.age = 31;
    // ^^^^^ error[E0616]: field `age` of struct `User` is private

    // Compile error: không dựng được struct literal vì có field private
    // let _ = model::User { name: "x".into(), age: 1 };
}

Quy tắc dẫn xuất quan trọng: nếu struct có bất kỳ field private nào, ngoài module không thể dựng struct literal cho nó. Buộc phải đi qua một fn new hoặc tương đương. Đây chính là cách Rust ép bạn dùng constructor để bảo toàn invariant — một dạng pattern controlled construction.

Best practice: với struct dùng làm data carrier đơn giản (DTO, request body), pub tất cả field để dễ dùng. Với struct mang invariant (ID không trùng, balance không âm, version chỉ tăng), giữ field private và expose getter + builder.

5

Enum Variant — Tự Pub Theo Enum

Khác hẳn struct, variant của pub enum tự động pub theo enum. Bạn không ghi pub trước từng variant — và thực tế nếu ghi, compiler sẽ báo lỗi cú pháp.

mod http {
    pub enum Status {
        Ok,
        NotFound,
        InternalError,
    }

    // Lỗi cú pháp nếu viết:
    // pub enum Bad {
    //     pub Ok, // ^^^ unnecessary visibility qualifier
    // }
}

fn main() {
    let s = http::Status::Ok;

    match s {
        http::Status::Ok => println!("ok"),
        http::Status::NotFound => println!("404"),
        http::Status::InternalError => println!("500"),
    }
}

Lý do của sự bất đối xứng giữa structenum nằm ở ngữ nghĩa: enum là một SET đóng các trạng thái — không có ý nghĩa "che giấu một số variant trong khi expose các variant khác". Nếu bạn muốn che một số trạng thái, đó là dấu hiệu nên tách thành hai enum, hoặc đưa biến thể đó thành struct riêng. Với field bên trong variant (variant kiểu struct, ví dụ Move { x: i32, y: i32 }) — các field này cũng tự pub theo enum, không cần ghi pub.

Hệ quả: khi viết một crate có enum public, hãy cẩn thận thêm variant mới. Vì variant tự pub, mọi thêm variant đều là breaking change với user dùng match exhaustive. Pattern phổ biến để tránh là gắn attribute #[non_exhaustive] lên enum — caller buộc phải có nhánh _ wildcard, cho phép bạn thêm variant mới mà không vỡ build user. Chi tiết ở các bài về API stability sau này.

6

pub(crate) — Visible Toàn Crate

Giữa hai cực private (chỉ trong module) và public (cả thế giới), Rust có một mức trung gian rất hữu ích: pub(crate). Item gắn pub(crate) visible trong toàn bộ crate hiện tại, nhưng không visible ra ngoài crate (tức user khi use my_crate::... sẽ không thấy).

// src/lib.rs (crate gọi là `myapp`)
mod util {
    // Visible khắp myapp, nhưng dùng myapp::util::helper từ crate khác sẽ lỗi
    pub(crate) fn helper(input: &str) -> String {
        format!("[helper] {input}")
    }
}

mod api {
    use crate::util::helper; // OK - cùng crate myapp

    pub fn process(input: &str) -> String {
        helper(input)
    }
}

pub use api::process;

// Trong một crate khác:
// use myapp::util::helper;
// ^^^^^^^^^^^^^^^^^^^^^^^^
// error[E0603]: function `helper` is private

pub(crate)idiom chuẩn cho internal API. Bạn có nhiều helper, nhiều type cần chia sẻ giữa các module bên trong một thư viện, nhưng không muốn lộ ra cho user vì chúng có thể đổi signature bất cứ lúc nào. Đặt pub(crate) giữ chúng linh hoạt mà vẫn dùng được mọi nơi trong crate.

Khi nào không nên dùng pub(crate)? Khi item chỉ dùng trong một module và child của nó — giữ private là đủ. Đừng pub(crate) "cho chắc" — visibility là tài liệu cho người đọc, ghi rộng hơn cần thiết là gây hiểu nhầm về phạm vi sử dụng.

7

pub(super) — Visible Parent Module

pub(super) hẹp hơn pub(crate): item visible tới parent module trực tiếp (và tất nhiên là trong module hiện tại), nhưng không lan ra anh em hay grandparent.

mod outer {
    mod inner {
        // Visible cho outer (parent), nhưng không cho main hay sibling khác của outer
        pub(super) fn internal() {
            println!("from inner");
        }

        // Helper riêng cho test - parent thấy được để gọi từ test util
        #[cfg(test)]
        pub(super) fn test_seed() -> u32 {
            42
        }
    }

    pub fn run() {
        inner::internal(); // OK - outer là parent của inner
    }
}

fn main() {
    outer::run();

    // Lỗi: main không phải parent của inner
    // outer::inner::internal();
}

Use case điển hình của pub(super):

  • Test helper: một submodule chứa fixture, mock factory; parent module dùng để viết test nhưng không expose lên cao hơn nữa.
  • Logical grouping: tách logic phụ trợ thành submodule cho gọn file, nhưng coordinator nằm ở parent vẫn cần truy cập. Lúc này pub(crate) là quá rộng — pub(super) mới chính xác phạm vi dự định.
  • Internal callback: child cung cấp một hàm cho parent gọi vào, không phải API cho cả thế giới.

Lưu ý: pub(super) chỉ tới parent trực tiếp, không truyền tiếp lên ông bà. Nếu cần expose lên cao hơn, dùng pub(in path) (mục 8) hoặc pub(crate).

8

pub(in path) — Granular Visibility

Khi pub(super) hẹp quá còn pub(crate) rộng quá, Rust cho phép chỉ định chính xác module nào được thấy item, qua cú pháp pub(in path::to::module). Path phải là một ancestor (tổ tiên) của module khai báo item — không thể "mở visibility" cho module ngẫu nhiên.

mod api {
    pub mod routes {
        pub mod users {
            // Visible khắp api::routes (gồm users, posts, comments...)
            // nhưng không visible ra ngoài api::routes
            pub(in crate::api::routes) fn build_handler() {
                println!("building user handler");
            }
        }

        pub mod posts {
            pub fn list() {
                // OK: posts cũng nằm trong crate::api::routes
                super::users::build_handler();
            }
        }
    }
}

fn main() {
    api::routes::posts::list();

    // Lỗi: main không nằm trong crate::api::routes
    // api::routes::users::build_handler();
}

pub(in path) dùng khi bạn có một subsystem nhiều module ngang hàng (ví dụ routes/users, routes/posts, routes/auth) cần chia sẻ helper giữa nhau, nhưng không muốn helper đó visible cho phần khác của crate (controller, service, repository). Đây là dạng visibility hiếm gặp nhất — rarely needed — vì đa phần pub(crate) đã đủ. Nhưng khi cần, nó cứu bạn khỏi việc đặt module ở chỗ kỳ quặc chỉ để chỉnh visibility.

Quy ước: cố gắng dùng pub(crate) trước; chỉ rút xuống pub(in path) khi thực sự thấy việc lộ ra cả crate gây nhầm lẫn. Đọc pub(in crate::api::routes) ở dòng đầu file ngay lập tức nói lên "chỉ subsystem này dùng, đừng tìm cách gọi từ chỗ khác".

9

Re-export Pattern Preview — pub use

Một pattern liên quan trực tiếp đến visibility nhưng đáng dành nguyên một bài để học là re-export qua pub use. Ý tưởng: bên trong crate, code chia thành nhiều module sâu cho dễ tổ chức; nhưng khi expose ra public API, bạn muốn user gọi my_crate::User chứ không phải my_crate::model::user::types::User.

// src/model/user.rs
pub struct User {
    pub name: String,
}

// src/model/mod.rs (hoặc src/model.rs)
pub mod user;

// src/lib.rs
mod model;
// Re-export: nâng User lên cấp crate root
pub use model::user::User;

// Bây giờ user gọi:
// use my_crate::User;     // OK - flatten
// use my_crate::model::user::User; // vẫn OK nhưng không cần

pub use kết hợp cả use (bring path vào scope hiện tại) và pub (expose item ra ngoài). Item được nâng lên cùng visibility với chỗ pub use đó — bạn có thể đặt ở root lib.rs để expose ra ngoài crate, hoặc đặt sâu hơn với pub(crate) use để re-export nội bộ.

Các pattern thực tế:

  • Flatten API: như ví dụ trên — user không cần biết cấu trúc thư mục nội bộ.
  • Facade: lib.rs trở thành mặt tiền duy nhất; thay đổi vị trí User trong tree không phá user vì re-export vẫn ổn.
  • Prelude: tạo module prelude gom các symbol thường dùng — user chỉ cần use my_crate::prelude::*;.

Bài 116 pub-use-reexport sẽ đi sâu vào từng dạng trên, kèm ví dụ thực tế từ lib.rs của các crate nổi tiếng. Trong bài này, chỉ cần nhận diện pub use để khi đọc code không nhầm lẫn.

10

Tổng Kết

  • Default private: mọi item Rust mặc định chỉ visible trong module khai báo và child module — ép encapsulation, hỗ trợ tự do refactor.
  • pub cơ bản: đặt trước fn/struct/enum/mod/const/static/trait để expose ra ngoài; cả chuỗi module từ root đến item phải đều pub mới thực sự visible.
  • Struct field: pub trên struct chỉ expose tên type; mỗi field phải pub riêng. Struct có field private không dựng được literal từ ngoài — ép qua constructor.
  • Enum variant: tự động pub theo enum, không ghi pub trước variant. Field bên trong variant kiểu struct cũng tự pub.
  • pub(crate): idiom cho internal API — visible khắp crate nhưng ẩn với user bên ngoài. Đa phần helper nội bộ dùng mức này.
  • pub(super): visible tới parent trực tiếp — phù hợp test helper, internal callback, logical grouping mà pub(crate) quá rộng.
  • pub(in path): visible đúng một path ancestor cụ thể — granular nhất, hiếm dùng, để chia sẻ helper giữa subsystem ngang hàng.
  • pub use: re-export — flatten public API, facade pattern. Học sâu ở Bài 116.
  • Quy tắc thực hành: bắt đầu từ private, mở rộng dần khi thực sự cần. Visibility là tài liệu — ghi rộng hơn cần thiết gây hiểu nhầm về phạm vi.
11

Bài Tập Củng Cố

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

  1. Bạn có pub struct Account { balance: i64 } trong module bank. Từ main, bạn cố ghi Account { balance: 100 }acc.balance = 200. Cả hai có compile được không? Giải thích.
  2. Tại sao pub enum Color { Red, Green, Blue } không cần ghi pub trước từng variant, trong khi pub struct Point { x: i32, y: i32 } lại cần pub cho từng field?
  3. Bạn đang viết library parser và có hàm lex_internal mà mọi module trong crate đều dùng nhưng tuyệt đối không muốn user thấy. Visibility nào phù hợp nhất? Vì sao không phải pub hay pub(super)?
  4. Module api::routes::users có hàm helper build_handler chỉ dùng cho các module ngang hàng trong api::routes (posts, comments...). Viết visibility chính xác cho build_handler.
  5. Một crate có cấu trúc src/model/user.rs chứa pub struct User. User crate muốn gọi use my_crate::User thay vì use my_crate::model::user::User. Phải viết gì ở src/lib.rs?
Đáp án
  1. Cả hai đều không compile. Field balance mặc định private dù struct Accountpub. Struct literal Account { balance: 100 } đòi truy cập trực tiếp field — lỗi field `balance` of struct `Account` is private. Phép gán acc.balance = 200 cũng lỗi cùng kiểu. Muốn dùng phải đi qua constructor như Account::new(100) hoặc method deposit mà module bank expose.
  2. ngữ nghĩa kiểu khác nhau. Struct là một record với các field, một số field có thể nhạy cảm (giữ invariant) — phải che riêng từng field. Enum là một SET đóng các trạng thái, không có ý nghĩa "che một số trạng thái khỏi user" — nếu cần che, hãy tách thành enum khác hoặc struct riêng. Field bên trong variant struct-style cũng tự pub theo enum cùng lý do đó.
  3. pub(crate) fn lex_internal(). Đây là idiom chuẩn cho internal API: visible khắp crate parser để các module dùng được, nhưng ẩn với user khi họ use parser::.... pub sẽ làm user thấy được — không mong muốn. pub(super) chỉ visible với parent trực tiếp, không lan ra các module ngang hàng — quá hẹp.
  4. pub(in crate::api::routes) fn build_handler() { ... }. Visible khắp api::routes (cho users, posts, comments) nhưng không visible cho phần khác của crate. pub(super) chỉ tới api::routes (parent trực tiếp) — đúng với code trong file users.rs nhưng nếu hàm được khai báo sâu hơn (ví dụ api::routes::users::helpers) thì pub(super) không đủ — pub(in crate::api::routes) mới chính xác phạm vi mong muốn ở mọi vị trí trong subsystem.
  5. src/lib.rs: mod model; (declaration để compiler thấy module tree) rồi pub use model::user::User; để re-export User lên crate root. Lưu ý cả mod user; trong src/model.rs (hoặc src/model/mod.rs) cũng phải có để khai báo submodule. Chi tiết pattern này ở Bài 116.
12

Bài Tiếp Theo

Bài 114: use Keyword — Bring Path Vào Scope — sau khi đã hiểu cách kiểm soát visibility ai thấy được gì, bài kế tiếp dạy cách rút ngắn path khi đã thấy. use std::io::Write; để gọi Write thay vì std::io::Write mỗi lần; use foo::bar as baz để đổi tên tránh xung đột; group use std::{io, fs}; để gom; và glob use foo::* tiện nhưng nên tránh trong production. pubuse phối hợp với nhau là hai mặt của cùng một bài toán tổ chức code Rust.