Constants #
Rust provides two keywords for defining values that don’t change, beyond ordinary variables: const and static. They look similar — both are global, both can’t be reassigned — but the way they work at the memory level is very different, and choosing the wrong one can affect both performance and program safety. On top of that, Rust also has const fn: functions that can be evaluated entirely at compile time, shifting computation that normally happens at runtime into the build phase. This article covers all three mechanisms in depth — how they work, their limits, their differences, and the right usage patterns for each.
Why Constants Matter #
Before diving into syntax, there’s a deeper reason why constants — not just ordinary immutable variables — matter in program design.
Immutable variables (let x = 5) have a limited scope: they live inside the function or block where they’re declared. Constants can be declared at module or crate level, available everywhere, and their value is known at compile time. This unlocks several benefits:
// Without constants — "magic numbers" scattered across the codebase
fn hitung_kapasitas(jumlah: usize) -> usize {
jumlah * 1024 // 1024 what? Why 1024?
}
fn validasi_ukuran(ukuran: usize) -> bool {
ukuran <= 1024 // the same number, but no consistency guarantee
}
// With constants — one source of truth, meaningful names
const UKURAN_BLOK_BYTE: usize = 1024;
fn hitung_kapasitas(jumlah: usize) -> usize {
jumlah * UKURAN_BLOK_BYTE
}
fn validasi_ukuran(ukuran: usize) -> bool {
ukuran <= UKURAN_BLOK_BYTE
}
If UKURAN_BLOK_BYTE ever needs to change to 4096, you change one line — every reference updates with it. Without constants, you’d have to find and replace every occurrence of the number 1024 manually, risking missing some.
const Constants
#
const defines a value that is evaluated entirely at compile time. The compiler doesn’t allocate a memory slot for the constant — it inlines the value directly into every place the constant is used, like copy-paste performed automatically by the compiler.
Syntax and Basic Rules #
// Module-level declaration — the most common
const NAMA_KONSTANTA: Tipe = ekspresi_konstan;
// Real examples
const PI: f64 = 3.141_592_653_589_793;
const MAKS_KONEKSI: u32 = 100;
const NAMA_APP: &str = "RustApp";
const VERSI: (u8, u8, u8) = (1, 0, 0); // tuples are also valid
Three rules that can’t be broken for const:
- The type must always be explicit — there’s no type inference for
const - The value must be a constant expression — it must be fully evaluable at compile time without runtime
- It can’t be
mut—const mutisn’t valid syntax in Rust
// ANTI-PATTERN: no type annotation
const MAKS = 100; // error[E0121]: type annotations needed
// ANTI-PATTERN: a value that can't be evaluated at compile time
const WAKTU_SEKARANG: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(); // error: cannot call non-const fn `SystemTime::now` in constants
// CORRECT: literals or expressions that are fully constant
const MAKS: u32 = 100;
const DUA_KALI_MAKS: u32 = MAKS * 2; // using another constant — valid
Constant Scope #
const can be declared in any scope: global (module/crate level), inside functions, even inside blocks. Function-level scope is useful for constants that only matter in a local context.
const GRAVITASI: f64 = 9.81; // available across the whole module
fn hitung_energi_potensial(massa_kg: f64, ketinggian_m: f64) -> f64 {
const SATUAN: &str = "Joule"; // only relevant here
let energi = massa_kg * GRAVITASI * ketinggian_m;
println!("Potential energy: {} {}", energi, SATUAN);
energi
}
fn main() {
hitung_energi_potensial(70.0, 10.0);
println!("Earth's gravity: {} m/s²", GRAVITASI);
// println!("{}", SATUAN); // error: SATUAN isn't available here
}
Constants in Struct Implementations #
const can be declared inside an impl block — useful for values closely tied to a specific type:
struct Lingkaran {
radius: f64,
}
impl Lingkaran {
const PI: f64 = std::f64::consts::PI;
fn luas(&self) -> f64 {
Self::PI * self.radius * self.radius
}
fn keliling(&self) -> f64 {
2.0 * Self::PI * self.radius
}
}
struct Persegi {
sisi: f64,
}
impl Persegi {
// Diagonal-to-side ratio: √2
const RASIO_DIAGONAL: f64 = std::f64::consts::SQRT_2;
fn diagonal(&self) -> f64 {
self.sisi * Self::RASIO_DIAGONAL
}
}
fn main() {
let l = Lingkaran { radius: 5.0 };
println!("Area: {:.4}", l.luas());
println!("Circumference: {:.4}", l.keliling());
let p = Persegi { sisi: 10.0 };
println!("Diagonal: {:.4}", p.diagonal());
}
Constants in Traits #
const can also be part of a trait definition — every type that implements the trait can provide a different constant value:
trait Batas {
const MIN: i32;
const MAKS: i32;
fn dalam_batas(&self, nilai: i32) -> bool;
}
struct SuhuCelsius;
struct SuhuFahrenheit;
impl Batas for SuhuCelsius {
const MIN: i32 = -273; // absolute zero in Celsius
const MAKS: i32 = 1_000_000; // an estimate of a star's core temperature
fn dalam_batas(&self, nilai: i32) -> bool {
nilai >= Self::MIN && nilai <= Self::MAKS
}
}
impl Batas for SuhuFahrenheit {
const MIN: i32 = -459; // absolute zero in Fahrenheit
const MAKS: i32 = 1_800_032;
fn dalam_batas(&self, nilai: i32) -> bool {
nilai >= Self::MIN && nilai <= Self::MAKS
}
}
fn main() {
let c = SuhuCelsius;
println!("100°C valid: {}", c.dalam_batas(100));
println!("-300°C valid: {}", c.dalam_batas(-300));
println!("Celsius lower bound: {}°C", SuhuCelsius::MIN);
}
static Constants
#
static defines a global value that has one fixed memory location for the entire lifetime of the program. Unlike const which is inlined, a static value really exists as a single object in memory — you can take references to it, and those references stay valid as long as the program runs.
Immutable static
#
static NAMA_VERSI: &str = "1.0.0-beta";
static BUFFER_DEFAULT: [u8; 8] = [0; 8];
static TABEL_KODE: [(u8, &str); 3] = [
(200, "OK"),
(404, "Not Found"),
(500, "Internal Server Error"),
];
fn main() {
println!("Version: {}", NAMA_VERSI);
// Taking a reference to a static — the reference has 'static lifetime
let r: &'static str = NAMA_VERSI;
println!("Reference: {}", r);
for (kode, pesan) in &TABEL_KODE {
println!("{}: {}", kode, pesan);
}
}
The 'static lifetime appearing here means the reference stays valid for the whole program run — there’s no chance of a dangling reference because the static data always exists.
When static Is Better Than const
#
Use (immutable) static instead of const when:
// CORRECT to use static: large data that doesn't need to be copied at every use
static DAFTAR_NEGARA: &[&str] = &[
"Indonesia", "Malaysia", "Singapura", "Thailand", "Vietnam",
"Filipina", "Myanmar", "Kamboja", "Laos", "Brunei",
// ... hundreds of other countries
];
// With const, this value would be copied into every use site
// With static, all references point to the same single location
// CORRECT to use const: small values used frequently
const MAKS_RETRY: u8 = 3;
// Inlined = no indirection overhead, directly a literal value
flowchart TD
A{Need a constant?}
A --> B{Large data\nor need a reference\nwith a static lifetime?}
B -- Yes --> C[Use static\nOne memory location\n&'static T references are valid]
B -- No --> D{Does the value need to be computed\nfrom a complex expression\nat compile time?}
D -- Yes --> E[Use const fn\n+ const]
D -- No --> F[Use const\nInlined by the compiler\nMost efficient for small values]static mut — Global Mutable State
#
Rust allows static mut to define changeable global state, but with one strict requirement: every access must be inside an unsafe block.
static mut JUMLAH_PANGGILAN: u32 = 0;
fn catat_panggilan() {
unsafe {
JUMLAH_PANGGILAN += 1;
}
}
fn baca_jumlah() -> u32 {
unsafe { JUMLAH_PANGGILAN }
}
fn main() {
catat_panggilan();
catat_panggilan();
catat_panggilan();
println!("Called {} times", baca_jumlah()); // 3
}
Avoidstatic mutin production code. Every access tostatic mutisunsafebecause there’s no concurrency safety guarantee — two threads accessing the same variable at once without synchronization is a data race, which is undefined behavior in Rust. For global state that needs to change, use safe alternatives:Mutex<T>,RwLock<T>, or atomic types fromstd::sync::atomic.
Safe Alternatives to static mut
#
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex;
// static mut replacement for a counter — atomic types, safe without unsafe
static JUMLAH_PANGGILAN: AtomicU32 = AtomicU32::new(0);
// static mut replacement for complex data — Mutex
static KONFIGURASI: Mutex<Option<String>> = Mutex::new(None);
fn catat_panggilan() {
// No unsafe needed — atomic operations are already thread-safe
JUMLAH_PANGGILAN.fetch_add(1, Ordering::SeqCst);
}
fn set_konfigurasi(nilai: &str) {
// No unsafe needed — Mutex guarantees exclusive access
let mut lock = KONFIGURASI.lock().unwrap();
*lock = Some(nilai.to_string());
}
fn main() {
catat_panggilan();
catat_panggilan();
set_konfigurasi("mode=produksi");
println!("Calls: {}", JUMLAH_PANGGILAN.load(Ordering::SeqCst));
println!("Config: {:?}", KONFIGURASI.lock().unwrap());
}
const fn — Compile-Time Computation
#
const fn is a function that can be evaluated at compile time if all its arguments are constant values. Its result can be used to define const from expressions more complex than simple literals.
Basic const Functions
#
const fn kilo(n: u64) -> u64 {
n * 1_000
}
const fn mega(n: u64) -> u64 {
kilo(n) * 1_000 // calling another const fn — valid
}
const fn giga(n: u64) -> u64 {
mega(n) * 1_000
}
// All of these are evaluated at compile time — no runtime overhead
const SATU_KB: u64 = kilo(1);
const SATU_MB: u64 = mega(1);
const SATU_GB: u64 = giga(1);
const BATAS_FILE: u64 = mega(512); // 512 MB in bytes
fn main() {
println!("1 KB = {} bytes", SATU_KB);
println!("1 MB = {} bytes", SATU_MB);
println!("1 GB = {} bytes", SATU_GB);
println!("File size limit: {} bytes", BATAS_FILE);
}
const fn with Conditional Logic
#
Since Rust 1.46, const fn supports if, else, and simple loops:
const fn maks(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
const fn min(a: i32, b: i32) -> i32 {
if a < b { a } else { b }
}
const fn clamp(nilai: i32, bawah: i32, atas: i32) -> i32 {
maks(bawah, min(nilai, atas))
}
const fn faktorial(n: u64) -> u64 {
// Loops are also valid in const fn since Rust 1.46
let mut hasil = 1u64;
let mut i = 2u64;
while i <= n {
hasil *= i;
i += 1;
}
hasil
}
// All computed at compile time
const BATAS_BAWAH: i32 = maks(-100, 0); // 0
const BATAS_ATAS: i32 = min(1000, 500); // 500
const NILAI_VALID: i32 = clamp(750, 0, 500); // 500
const FAKTORIAL_10: u64 = faktorial(10); // 3628800
fn main() {
println!("Lower bound: {}", BATAS_BAWAH);
println!("Upper bound: {}", BATAS_ATAS);
println!("Value after clamping: {}", NILAI_VALID);
println!("10! = {}", FAKTORIAL_10);
}
Limitations of const fn
#
Not every operation can be done inside a const fn. These limits exist because some operations are inherently runtime-dependent:
// CANNOT be done in const fn:
// - Heap allocation (Box::new, Vec::new, String::from, etc.)
// - Floating-point arithmetic (in some older Rust versions)
// - Trait objects (dyn Trait)
// - Closures (in some contexts)
// - Complex exception/panic handling
const fn contoh_valid(n: u32) -> u32 {
// ✓ Integer arithmetic
let hasil = n * 2 + 1;
// ✓ Conditionals
if hasil > 100 { 100 } else { hasil }
}
// ANTI-PATTERN: trying to allocate on the heap in a const fn
const fn buat_string() -> String {
String::from("halo") // error: cannot call non-const fn in constants
}
const fn as an Ordinary Function
#
A function marked const can still be called at runtime like an ordinary function — it’s just also evaluable at compile time:
const fn pangkat_dua(n: u32) -> u32 {
n * n
}
const LIMA_KUADRAT: u32 = pangkat_dua(5); // compile-time: 25
fn main() {
let input: u32 = 7; // runtime value
let hasil = pangkat_dua(input); // runtime call — still valid
println!("7² = {}", hasil);
println!("5² = {}", LIMA_KUADRAT);
}
Comparing const, static, and let Variables
#
| Aspect | const | static | let (immutable) |
|---|---|---|---|
| Location | Inlined (no fixed memory slot) | One fixed memory location | Function stack frame |
| Scope | Module, function, block, impl | Global (crate-wide) | Local scope |
| Explicit type required | Yes | Yes | No (can be inferred) |
| Can be mutable | No | Yes (static mut, but dangerous) | Yes (let mut) |
| Lifetime | N/A (no memory location) | 'static | Depends on scope |
| Evaluation | Compile time | Compile time | Runtime |
| References | Each inline has a different reference | One &'static T reference | Local references |
| Best for | Small frequently used values | Large data / static references needed | Temporary local data |
Naming Conventions #
Rust uses SCREAMING_SNAKE_CASE for all constants — this is a convention enforced by the clippy linter and followed by the entire Rust ecosystem, including the standard library:
// ✓ CORRECT: SCREAMING_SNAKE_CASE
const MAKS_UKURAN_BUFFER: usize = 4096;
const TIMEOUT_KONEKSI_MS: u64 = 5_000;
static KUNCI_ENKRIPSI: &[u8] = b"kunci-rahasia-32-karakter-panjang";
// ✗ ANTI-PATTERN: using other conventions
const maxUkuranBuffer: usize = 4096; // camelCase — clippy warning
const max_ukuran_buffer: usize = 4096; // snake_case — clippy warning
const MaxUkuranBuffer: usize = 4096; // PascalCase — clippy warning
Underscores as digit separators are strongly recommended for large numbers — far easier to read:
// ✗ Hard to read
const POPULASI_BUMI: u64 = 8000000000;
// ✓ Easy to read
const POPULASI_BUMI: u64 = 8_000_000_000;
const SATU_JUTA: u32 = 1_000_000;
const BATAS_PORT: u16 = 65_535;
Real-World Usage Patterns #
Application Configuration #
// config.rs — all configuration constants in one place
pub const VERSI_API: &str = "v2";
pub const HOST_DEFAULT: &str = "127.0.0.1";
pub const PORT_DEFAULT: u16 = 8080;
pub const MAKS_KONEKSI_DB: u32 = 20;
pub const TIMEOUT_REQUEST_MS: u64 = 30_000;
pub const MAKS_UKURAN_BODY_BYTES: usize = 10 * 1024 * 1024; // 10 MB
fn main() {
println!(
"Server running at {}:{} (API {})",
HOST_DEFAULT, PORT_DEFAULT, VERSI_API
);
println!("Timeout: {} ms", TIMEOUT_REQUEST_MS);
println!("Max body: {} bytes", MAKS_UKURAN_BODY_BYTES);
}
Compile-Time Lookup Tables #
// Sine table for angles 0°, 30°, 45°, 60°, 90° — computed once at compile time
const TABEL_SIN: [f64; 5] = [0.0, 0.5, 0.7071067811865476, 0.8660254037844386, 1.0];
const SUDUT_DERAJAT: [u32; 5] = [0, 30, 45, 60, 90];
fn main() {
for (sudut, sin) in SUDUT_DERAJAT.iter().zip(TABEL_SIN.iter()) {
println!("sin({}°) = {:.4}", sudut, sin);
}
}
Sized Arrays from Constants #
One advantage of const that ordinary variables can’t do — using it as an array size:
const KAPASITAS_BUFFER: usize = 256;
const JUMLAH_WORKER: usize = 4;
fn main() {
// Array sizes must be known at compile time — const makes this possible
let buffer: [u8; KAPASITAS_BUFFER] = [0; KAPASITAS_BUFFER];
let worker_ids: [usize; JUMLAH_WORKER] = [0, 1, 2, 3];
println!("Buffer: {} bytes", buffer.len());
println!("Workers: {:?}", worker_ids);
// ANTI-PATTERN: using a let variable as an array size
let kapasitas = 256;
// let buffer2: [u8; kapasitas] = [0; kapasitas]; // error: expected constant, found local variable
}
Summary #
constis inlined by the compiler — no fixed memory location; the value is copied into every use site. Ideal for small values like numbers, short strings, and tuples.statichas one memory location — every reference to astaticpoints to the same place. Use it for large data or when you need a&'static Treference.- The type is always required to be explicit for
constandstatic— no type inference like withlet.const mutdoesn’t exist —constcan’t be mutable at all.static mutexists but is dangerous and requiresunsafefor every access.- Avoid
static mut— useAtomicTfor counters/flags,Mutex<T>orRwLock<T>for complex state that needs to change from many places.const fnmoves computation to compile time — a function can be evaluated at compile time if its arguments are constant. Supports if-else and while since Rust 1.46.constcan be used as an array size — one of the main advantages ofconstover ordinaryletvariables.- Use
SCREAMING_SNAKE_CASEfor all constants — the official Rust convention enforced by clippy.- Underscores as digit separators (
1_000_000) are strongly recommended for large numbers so they’re easy to read.