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 [email protected]. I’d greatly appreciate your feedback! :)