Module & Cargo — Rust

Module & Cargo di Rust Saat project mulai besar, kamu butuh cara mengorganisir kode menjadi file-file terpisah dan mengelola dependency. Rust melakukannya…

Module membagi kode jadi bagian-bagian bernama, masing-masing dengan isi yang privat dan isi yang dibuka untuk bagian lain. Cargo adalah alat yang membangun, menjalankan, mengetes, dan mengambilkan library untuk project-mu. Bayangin Kedai Martabak Bang Rusdi yang makin ramai dan dibagi jadi beberapa ruangan: dapur, kasir, gudang. Tiap ruangan punya barang yang cuma untuk orang di dalamnya, misalnya resep rahasia di dapur, dan sebuah loket untuk melayani ruangan lain. Denah ruangannya itu module tree, barang di balik pintu itu privat, loketnya itu pub. Surat izin usaha kedai plus daftar pemasoknya itu Cargo.toml, dan pasar tempat semua pemasok berjualan itu crates.io.

🧪 Tebakan cepat: di dalam mod menu { ... } ada pub fn harga dan fn resep_rahasia tanpa pub. main memanggil menu::resep_rahasia(). Lolos compile, karena masih satu file? Tebak dulu, jawabannya ada di bagian "pub: semuanya privat sampai dibuka".

mod: ruangan di dalam satu file

mod menu {
    pub fn harga(nama: &str) -> Option<u32> {
        match nama {
            "martabak manis" => Some(32000),
            "martabak telur" => Some(38000),
            "es teh" | "es jeruk" => Some(5000),
            _ => None,
        }
    }
}

fn main() {
    println!("{:?}", menu::harga("es teh"));
}

Output-nya Some(5000). mod menu { ... } membuat module bernama menu, dan isinya dipanggil lewat path menu::harga, dengan :: seperti di String::from. Seluruh program (satu cargo new = satu crate) adalah pohon module, dan akarnya main.rs.

pub: semuanya privat sampai dibuka

Ini kode tebakan tadi, dengan menu di atas ditambah satu fungsi:

    fn resep_rahasia() -> String {
        String::from("telur bebek, daun bawang, bumbu Bang Rusdi")
    }

dan main yang memanggil println!("{}", menu::resep_rahasia());:

error[E0603]: function `resep_rahasia` is private
  --> src/main.rs:18:26
   |
18 |     println!("{}", menu::resep_rahasia());
   |                          ^^^^^^^^^^^^^ private function
   |
note: the function `resep_rahasia` is defined here
  --> src/main.rs:11:5
   |
11 |     fn resep_rahasia() -> String {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Gak lolos, walaupun satu file. Itu jawaban tebakannya. Di Rust semua item privat secara default: fungsi, struct, field, dan module cuma terlihat di module tempat ia ditulis dan module-module di dalamnya. Yang memutuskan bukan file, tapi module.

Module di file sendiri

Begitu module membesar, pindahkan tiap module ke filenya sendiri:

kedai/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── menu.rs
    └── kasir/
        ├── mod.rs
        └── struk.rs
// src/main.rs
mod kasir;
mod menu;

fn main() {
    let pesanan = ["martabak manis", "martabak manis", "martabak telur"];
    println!("Total: {}", kasir::total(&pesanan));
    kasir::struk::cetak("Sari", &pesanan);
}
// src/menu.rs
/// Harga satu menu, atau `None` kalau gak ada di menu.
pub fn harga(nama: &str) -> Option<u32> {
    match nama {
        "martabak manis" => Some(32000),
        "martabak telur" => Some(38000),
        "es teh" | "es jeruk" => Some(5000),
        _ => None,
    }
}
// src/kasir/mod.rs
pub mod struk;

use crate::menu;

/// Total harga semua menu di pesanan. Menu yang gak dikenal dihitung 0.
pub fn total(pesanan: &[&str]) -> u32 {
    pesanan.iter().map(|m| menu::harga(m).unwrap_or(0)).sum()
}
// src/kasir/struk.rs
pub fn cetak(pelanggan: &str, pesanan: &[&str]) {
    println!("{} bayar {}", pelanggan, super::total(pesanan));
}
$ cargo run
   Compiling kedai v0.1.0 (/home/kamu/kedai)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/kedai`
Total: 102000
Sari bayar 102000

use dan path: crate::, super::, self::

Di dalam kasir, nama menu gak dikenal begitu saja, karena menu itu saudara, bukan anak. Path-nya bisa ditulis tiga cara:

use crate::menu; menaruh nama menu di module itu, jadi cukup menulis menu::harga. Tanpa use itu, menu::harga(m) di kasir ditolak dengan E0433 cannot find module or crate `menu` in this scope, dan compiler menyarankan use crate::menu;. use std::collections::HashMap; di pelajaran HashMap itu hal yang sama untuk standard library.

Cargo.toml dan dependency

cargo new kedai membuat folder kedai berisi Cargo.toml, src/main.rs, dan .gitignore. Cargo.toml adalah manifest project: identitas package, edition, dan daftar dependency.

[package]
name = "kedai"
version = "0.1.0"
edition = "2024"

[dependencies]
chrono = "0.4.45"

Baris chrono = "0.4.45" ditulis oleh cargo add chrono: Cargo mencari versi terbaru crate tanggal dan jam itu di crates.io (Adding chrono v0.4.45 to dependencies), lalu mengunci versi semua paket yang dibutuhkan (Locking 37 packages). Lalu di struk.rs:

use chrono::Local;

pub fn cetak(pelanggan: &str, pesanan: &[&str]) {
    let tanggal = Local::now().format("%d-%m-%Y");
    println!("{} | {} bayar {}", tanggal, pelanggan, super::total(pesanan));
}

cargo run kini mencetak 12-10-2026 | Sari bayar 102000, dengan tanggal hari kamu menjalankannya. Hal yang perlu diingat:

Perintah cargo sehari-hari

$ cargo check
    Checking kedai v0.1.0 (/home/kamu/kedai)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
$ cargo build --release
   Compiling kedai v0.1.0 (/home/kamu/kedai)
    Finished `release` profile [optimized] target(s) in 0.78s

Test ditulis di file yang sama, di bagian bawah menu.rs:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn harga_martabak_telur() {
        assert_eq!(harga("martabak telur"), Some(38000));
    }

    #[test]
    fn kopi_gak_ada() {
        assert_eq!(harga("kopi"), None);
    }
}
$ cargo test
   Compiling kedai v0.1.0 (/home/kamu/kedai)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/main.rs (target/debug/deps/kedai-9c40172ca2dacfc5)

running 2 tests
test menu::tests::kopi_gak_ada ... ok
test menu::tests::harga_martabak_telur ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

#[cfg(test)] membuat module tests cuma ikut di-compile saat cargo test, dan use super::*; membawa harga dari menu ke dalamnya. Test berjalan bersamaan, jadi urutan barisnya bisa beda tiap kali.

Dua alat lagi. cargo clippy mencari kode yang benar tapi bisa lebih baik. Kalau total diawali if pesanan.len() == 0 { return 0; }:

warning: length comparison to zero
 --> src/kasir/mod.rs:7:8
  |
7 |     if pesanan.len() == 0 {
  |        ^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `pesanan.is_empty()`

cargo fmt merapikan spasi dan indentasi semua file ke gaya standar Rust; cargo fmt --check cuma menunjukkan bedanya, misalnya pub fn cetak(pelanggan:&str,pesanan:&[&str]){ yang akan jadi pub fn cetak(pelanggan: &str, pesanan: &[&str]) {.

📌 Workspace dan menerbitkan crate

Kalau kedai tumbuh jadi beberapa crate, misalnya aplikasi kasir dan library menu yang juga dipakai aplikasi pesan antar, satukan dalam workspace: satu Cargo.toml di folder atas berisi [workspace] dengan members = ["kasir", "menu"]. Semua anggotanya berbagi satu Cargo.lock dan satu folder target/, jadi dependency yang sama cukup di-compile sekali, dan cargo test di folder atas mengetes semuanya.

Untuk menerbitkan crate ke crates.io: buat akun (login lewat GitHub), jalankan cargo login dengan token dari situsnya, lengkapi description dan license di [package], lalu cargo publish. Versi yang sudah terbit gak bisa ditimpa; kalau ada yang salah, terbitkan versi baru, dan tandai versi lama dengan cargo yank supaya project baru gak memakainya. Panduan lengkapnya ada di The Cargo Book, bab "Publishing on crates.io".

⚠️ Jebakan yang sering ketemu

TL;DR: mod menu { ... } atau mod menu; (isinya di src/menu.rs) membuat module, dan isinya dipanggil lewat path menu::harga. Semua item privat sampai diberi pub (E0603), pub(crate) membukanya untuk crate ini saja, dan field struct tetap privat kecuali ditandai pub (E0451). Module dengan anak memakai src/kasir/mod.rs atau src/kasir.rs, jangan dua-duanya (E0761). Path mulai dari akar dengan crate::, dari induk dengan super::, dari module ini dengan self::, dan use menyingkatnya. Cargo.toml adalah manifest: [package] (name, version, edition 2024) dan [dependencies] (cargo add chrono menulis chrono = "0.4.45"), sedangkan versi persisnya dicatat Cargo.lock. Perintah harian: cargo build, run, check (cuma memeriksa, paling cepat), test, build --release, doc, clippy, dan fmt.

Yang akan kamu pelajari