Rust Style Guide
Introduction
Writing code that works is only half the battle. Writing code that is clean, maintainable, and follows community standards is equally important. This Rust Style Guide will help you write idiomatic Rust code that not only functions correctly but is also easy to read, maintain, and collaborate on.
Whether you're just starting with Rust or looking to improve your coding style, this guide covers the essential conventions and best practices embraced by the Rust community. Following these guidelines will help you write more "Rustic" code and make your programming journey more enjoyable and productive.
Naming Conventions
Variables and Functions
In Rust, we use snake_case
for variables, functions, and module names:
// Good
let user_name = "rustacean";
fn calculate_total(items: &[Item]) -> f64 {
// function body
}
// Not idiomatic
let UserName = "rustacean";
fn CalculateTotal(items: &[Item]) -> f64 {
// function body
}
Types
For types (structs, enums, traits, and type aliases), use PascalCase
:
// Good
struct UserProfile {
username: String,
email: String,
}
enum ConnectionState {
Connected,
Disconnected,
Connecting,
}
trait DataProcessor {
fn process(&self, data: &[u8]) -> Result<Vec<u8>, ProcessError>;
}
// Not idiomatic
struct user_profile {
username: String,
email: String,
}
Constants
For constants and static variables, use SCREAMING_SNAKE_CASE
:
// Good
const MAX_CONNECTIONS: u32 = 100;
static DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
// Not idiomatic
const maxConnections: u32 = 100;
static default_timeout: Duration = Duration::from_secs(30);
Code Organization
Module Structure
Organize your code into modules with clear purposes:
// lib.rs or main.rs
mod config; // Configuration handling
mod models; // Data structures
mod utils; // Utility functions
mod handlers; // Request/event handlers
Each module should have a single responsibility, making your codebase easier to navigate.
File Structure
Follow this common pattern for structuring module files:
src/
├── main.rs
├── config.rs
├── models/
│ ├── mod.rs
│ ├── user.rs
│ └── product.rs
└── utils/
├── mod.rs
├── formatting.rs
└── validation.rs
For larger modules that need submodules, create a directory with the module name and include a mod.rs
file.
Formatting and Whitespace
Indentation
Use 4 spaces for indentation, not tabs:
fn main() {
let x = 5;
if x > 0 {
println!("x is positive");
}
}
Line Length
Keep lines to a reasonable length (around 80-100 characters) for better readability. Break long lines at logical points:
// Too long
let result = some_function_with_a_very_long_name(first_parameter, second_parameter, third_parameter, fourth_parameter);
// Better
let result = some_function_with_a_very_long_name(
first_parameter,
second_parameter,
third_parameter,
fourth_parameter,
);
Whitespace
Use blank lines to separate logical sections of code:
fn process_data(data: &[u8]) -> Result<Vec<u8>, ProcessError> {
// Validate input
if data.is_empty() {
return Err(ProcessError::EmptyInput);
}
// Process data
let mut result = Vec::with_capacity(data.len());
for byte in data {
result.push(process_byte(*byte)?);
}
// Return processed result
Ok(result)
}
Comments and Documentation
Regular Comments
Use //
for single-line comments and /* ... */
for multi-line comments:
// This is a single-line comment
/*
This is a multi-line comment
that spans several lines
*/
Documentation Comments
For documentation that should be included in generated docs, use ///
for items and //!
for modules:
/// Calculates the sum of all elements in the slice.
///
/// # Examples
///
/// ```
/// let numbers = [1, 2, 3, 4, 5];
/// let sum = calculate_sum(&numbers);
/// assert_eq!(sum, 15);
/// ```
fn calculate_sum(values: &[i32]) -> i32 {
values.iter().sum()
}
//! This module provides utilities for mathematical operations.
//!
//! It includes functions for basic arithmetic, statistics, and more.
Error Handling
Using Result and Option
Prefer using Result
and Option
over exceptions or sentinel values:
// Good
fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
if b == 0.0 {
return Err("Division by zero");
}
Ok(a / b)
}
// Usage
match divide(10.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(error) => eprintln!("Error: {}", error),
}
// Also good - using Option
fn find_user(id: u64) -> Option<User> {
// Return Some(user) if found, None otherwise
}
Error Propagation
Use the ?
operator for clean error propagation:
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Control Flow
If Expressions
Rust's if
is an expression, so you can use it to assign values:
// Good
let status = if connected {
"Connected"
} else {
"Disconnected"
};
// Instead of
let status;
if connected {
status = "Connected";
} else {
status = "Disconnected";
}
Match Expressions
Use match
for comprehensive pattern matching:
match value {
0 => println!("Zero"),
1 => println!("One"),
2..=5 => println!("Two to five"),
_ => println!("Other"),
}
For simple cases, consider using if let
or while let
:
// Instead of
match optional_value {
Some(value) => {
// Do something with value
},
None => {},
}
// Use this
if let Some(value) = optional_value {
// Do something with value
}
Memory Management
Ownership and Borrowing
Follow Rust's ownership model by preferring borrowing over cloning when possible:
// Good - borrows data
fn process(data: &[u8]) -> Result<(), ProcessError> {
// Process data without taking ownership
}
// Less efficient - clones data
fn process_clone(data: Vec<u8>) -> Result<(), ProcessError> {
// Takes ownership of a clone
}
// Usage
let data = vec![1, 2, 3];
process(&data)?; // Borrow, don't take ownership
process_clone(data.clone())?; // Clone when necessary
Lifetimes
Be explicit about lifetimes when necessary:
// Good - explicit lifetime annotation
struct DataProcessor<'a> {
reference: &'a [u8],
}
impl<'a> DataProcessor<'a> {
fn process(&self) -> Result<Vec<u8>, ProcessError> {
// Process self.reference
}
}
Real-World Example: Building a Configuration Manager
Let's build a simple configuration manager that demonstrates many of the style guidelines we've covered:
//! Configuration management module for Rust applications.
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::io;
/// Represents different types of configuration errors.
#[derive(Debug)]
pub enum ConfigError {
IoError(io::Error),
ParseError(String),
ValidationError(String),
}
impl From<io::Error> for ConfigError {
fn from(error: io::Error) -> Self {
ConfigError::IoError(error)
}
}
/// Stores application configuration with typed access to values.
pub struct Config {
values: HashMap<String, ConfigValue>,
}
/// Represents different types of configuration values.
#[derive(Debug, Clone)]
pub enum ConfigValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
List(Vec<ConfigValue>),
}
impl Config {
/// Creates a new empty configuration.
pub fn new() -> Self {
Config {
values: HashMap::new(),
}
}
/// Loads configuration from a file.
///
/// # Examples
///
/// ```
/// let config = Config::load_from_file("config.json")?;
/// let server_port = config.get_integer("server.port").unwrap_or(8080);
/// ```
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let content = fs::read_to_string(path)?;
Self::parse_content(&content)
}
/// Parses configuration content from a string.
fn parse_content(content: &str) -> Result<Self, ConfigError> {
// This is a simplified implementation
let mut config = Config::new();
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
// Skip empty lines and comments
if line.is_empty() || line.starts_with('#') {
continue;
}
// Parse key-value pairs (key=value format)
if let Some(pos) = line.find('=') {
let key = line[..pos].trim().to_string();
let value_str = line[pos+1..].trim();
let value = Self::parse_value(value_str)
.map_err(|e| ConfigError::ParseError(
format!("Line {}: {} - {}", line_num + 1, e, line)
))?;
config.values.insert(key, value);
} else {
return Err(ConfigError::ParseError(
format!("Line {}: Invalid format - {}", line_num + 1, line)
));
}
}
Ok(config)
}
/// Parses a string into a strongly-typed ConfigValue.
fn parse_value(value_str: &str) -> Result<ConfigValue, String> {
// Try parsing as different types
if value_str == "true" {
return Ok(ConfigValue::Boolean(true));
} else if value_str == "false" {
return Ok(ConfigValue::Boolean(false));
}
// Try integer
if let Ok(int_value) = value_str.parse::<i64>() {
return Ok(ConfigValue::Integer(int_value));
}
// Try float
if let Ok(float_value) = value_str.parse::<f64>() {
return Ok(ConfigValue::Float(float_value));
}
// Default to string
Ok(ConfigValue::String(value_str.to_string()))
}
/// Gets a string value from the configuration.
pub fn get_string(&self, key: &str) -> Option<&str> {
match self.values.get(key) {
Some(ConfigValue::String(s)) => Some(s),
_ => None,
}
}
/// Gets an integer value from the configuration.
pub fn get_integer(&self, key: &str) -> Option<i64> {
match self.values.get(key) {
Some(ConfigValue::Integer(i)) => Some(*i),
_ => None,
}
}
/// Gets a float value from the configuration.
pub fn get_float(&self, key: &str) -> Option<f64> {
match self.values.get(key) {
Some(ConfigValue::Float(f)) => Some(*f),
_ => None,
}
}
/// Gets a boolean value from the configuration.
pub fn get_boolean(&self, key: &str) -> Option<bool> {
match self.values.get(key) {
Some(ConfigValue::Boolean(b)) => Some(*b),
_ => None,
}
}
}
// Usage example
fn main() -> Result<(), ConfigError> {
// Sample config content
let config_content = r#"
# Server configuration
server.host=localhost
server.port=8080
# Feature flags
features.dark_mode=true
features.experimental=false
# Limits
limits.max_connections=100
limits.timeout=30.5
"#;
let config = Config::parse_content(config_content)?;
// Access configuration values with appropriate types
let host = config.get_string("server.host").unwrap_or("127.0.0.1");
let port = config.get_integer("server.port").unwrap_or(80);
let dark_mode = config.get_boolean("features.dark_mode").unwrap_or(false);
let timeout = config.get_float("limits.timeout").unwrap_or(15.0);
println!("Server: {}:{}", host, port);
println!("Dark mode: {}", dark_mode);
println!("Timeout: {:.1} seconds", timeout);
Ok(())
}
This example demonstrates:
- Proper naming conventions
- Comprehensive error handling
- Documentation comments
- Modular code organization
- Type safety
- Clean control flow
Visual Structure Comparison
Here's a visual comparison of good vs. poor Rust coding style:
Tools for Enforcing Style
The Rust ecosystem provides several tools to help you maintain a consistent style:
-
rustfmt: The official Rust formatter that automatically formats your code according to the community style guidelines.
bash# Install rustfmt
rustup component add rustfmt
# Format a file or project
cargo fmt -
clippy: A collection of lints to catch common mistakes and improve your Rust code.
bash# Install clippy
rustup component add clippy
# Run clippy on your project
cargo clippy -
rust-analyzer: A language server that provides real-time feedback and suggestions in your code editor.
These tools can be integrated into your development workflow and CI/CD pipeline to ensure consistent code quality.
Summary
Following a consistent style guide makes your Rust code more readable, maintainable, and idiomatic. Remember these key points:
- Use
snake_case
for variables and functions,PascalCase
for types, andSCREAMING_SNAKE_CASE
for constants - Organize your code into modules with clear responsibilities
- Write comprehensive documentation using
///
and//!
comments - Handle errors explicitly using
Result
andOption
- Use Rust's expression-oriented features like
if
andmatch
- Follow the ownership model, preferring borrowing over cloning when possible
- Use tools like
rustfmt
andclippy
to enforce consistent style
By adhering to these guidelines, you'll write Rust code that not only works well but is also a pleasure to read and maintain.
Additional Resources
- The Rust Programming Language Book - Contains style guidelines and best practices
- Rust by Example - Learn Rust with examples that follow best practices
- Rust API Guidelines - Guidelines for designing public APIs
- Rust Style Guide RFC - More detailed style guide
Exercises
-
Style Refactoring: Take a piece of Rust code that doesn't follow the style guide and refactor it to meet the guidelines.
-
Documentation Practice: Choose a module or function you've written and add comprehensive documentation comments.
-
Error Handling Upgrade: Find code that uses
unwrap()
orexpect()
and refactor it to use proper error handling withResult
and the?
operator. -
Module Organization: Organize a flat project structure into a hierarchical module structure following Rust conventions.
-
Code Review: Review open-source Rust projects and identify examples of good and poor style choices.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)