Skip to main content

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:

rust
// 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:

rust
// 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:

rust
// 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:

rust
// 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:

rust
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:

rust
// 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:

rust
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:

rust
// 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:

rust
/// 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:

rust
// 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:

rust
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:

rust
// 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:

rust
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:

rust
// 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:

rust
// 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:

rust
// 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:

rust
//! 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:

  1. 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
  2. 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
  3. 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, and SCREAMING_SNAKE_CASE for constants
  • Organize your code into modules with clear responsibilities
  • Write comprehensive documentation using /// and //! comments
  • Handle errors explicitly using Result and Option
  • Use Rust's expression-oriented features like if and match
  • Follow the ownership model, preferring borrowing over cloning when possible
  • Use tools like rustfmt and clippy 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​

Exercises​

  1. Style Refactoring: Take a piece of Rust code that doesn't follow the style guide and refactor it to meet the guidelines.

  2. Documentation Practice: Choose a module or function you've written and add comprehensive documentation comments.

  3. Error Handling Upgrade: Find code that uses unwrap() or expect() and refactor it to use proper error handling with Result and the ? operator.

  4. Module Organization: Organize a flat project structure into a hierarchical module structure following Rust conventions.

  5. 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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)