Question
Global Variables in Rust: statics, shared state, and better alternatives
Question
I understand that global variables are usually discouraged, but in some practical cases they can seem useful, especially when a value is central to the whole program.
While learning Rust, I am writing a small database test program using SQLite. In this example, I currently pass the database connection between many functions. Since there are around a dozen functions involved, I wondered whether using a global variable would be possible instead.
Is it possible, feasible, or advisable to use global variables in Rust?
Given code like this:
extern crate sqlite;
fn main() {
let db: sqlite::Connection = open_database();
if !insert_data(&db, insert_max) {
return;
}
}
Can I declare and use a global variable for the database connection?
I tried something like this:
extern crate sqlite;
static mut DB: Option<sqlite::Connection> = None;
fn main() {
DB = sqlite::open("test.db").expect("'test.db' should be readable");
println!("Database opened successfully");
create_table();
println!("Completed");
}
fn create_table() {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
match DB.exec(sql) {
Ok(_) => println!("Table created"),
Err(err) => println!("Execution of SQL failed: {}\nSql={}", err, sql),
}
}
But this produces compile errors such as:
error[E0308]: mismatched types
expected enum `std::option::Option`, found struct `sqlite::Connection`
error: no method named `exec` found for type `std::option::Option<sqlite::Connection>`
How should this be done correctly in Rust, and what is the idiomatic approach?
Short Answer
By the end of this page, you will understand how global variables work in Rust, why static mut is usually a poor choice, and why passing a database connection by reference is often the idiomatic design. You will also learn safer patterns for global shared state, such as static, lazy initialization, and synchronization types like Mutex and OnceLock.
Concept
Rust does support global data, but not in the same casual way as many other languages.
The main reason is safety. A mutable global variable can be read and written from many places, which makes code harder to reason about and can cause data races in concurrent programs. Rust is designed to prevent that.
The key global concepts in Rust
const
- Compile-time constant
- Inlined where used
- Cannot hold runtime-created values like a database connection
static
- A value stored in one fixed memory location for the entire program
- Can be global
- Usually immutable
Example:
static APP_NAME: &str = "My App";
static mut
- A mutable global variable
- Access is
unsafe - Very easy to misuse
- Usually avoided in idiomatic Rust
Example:
static mut COUNTER: i32 = 0;
This exists, but Rust makes it inconvenient on purpose because unrestricted mutable global state is dangerous.
Mental Model
Think of a Rust program like a workshop.
- A normal local variable is a tool on one worker's bench.
- Passing
&dbto a function is like handing the tool to another worker for a moment. - A global mutable variable is like putting one shared tool in the middle of the room and letting anyone grab and modify it at any time.
That shared tool sounds convenient, but it creates problems:
- Who is using it right now?
- Is someone changing it while another part of the program depends on it?
- Has it even been set up yet?
Rust tries to make shared mutable tools harder to use unless you add the right safety controls. That is why globals exist, but safe Rust prefers clearly passing references or using synchronized shared state.
Syntax and Examples
Preferred approach: pass the connection by reference
This is the most idiomatic solution for your example.
extern crate sqlite;
fn main() {
let db = sqlite::open("test.db").expect("'test.db' should be readable");
println!("Database opened successfully");
create_table(&db);
println!("Completed");
}
fn create_table(db: &sqlite::Connection) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
match db.exec(sql) {
Ok(_) => println!("Table created"),
Err(err) => println!("Execution of SQL failed: {}\nSql={}", err, sql),
}
}
Why this is good
- No global mutable state
- No
unsafe - Easy to understand
- Easy to test
If you really want a global: safe one-time initialization
Step by Step Execution
Consider this version:
extern crate sqlite;
fn main() {
let db = sqlite::open("test.db").expect("database should open");
create_table(&db);
}
fn create_table(db: &sqlite::Connection) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
db.exec(sql).expect("table creation should succeed");
}
Step by step
1. main starts
let db = sqlite::open("test.db").expect("database should open");
sqlite::open("test.db")tries to create a connection.- It returns a result-like value.
.expect(...)either extracts the connection or stops the program with an error message.
Real World Use Cases
Where global state appears in real Rust programs
Global state is sometimes useful for data that is truly application-wide, such as:
- configuration loaded once at startup
- logging setup
- metrics counters
- feature flags
- caches shared across the program
- singleton-like registries
Where passing references is usually better
For runtime resources like database connections, developers usually prefer:
- passing
&Connectionto helper functions - storing the connection inside an
AppStatestruct - passing that state through application layers
Example:
struct AppState {
db: sqlite::Connection,
}
fn create_table(state: &AppState) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
state.db.exec(sql).unwrap();
}
This is common in command-line tools, servers, and web applications because it keeps dependencies explicit.
In web apps and services
A real application might use:
- a connection pool shared through app state
- immutable global configuration
- a global logger
But a single mutable raw connection as a global is usually avoided unless carefully synchronized.
Real Codebase Usage
In real Rust codebases, developers usually avoid naked globals and use structured state patterns.
Common patterns
1. Dependency passing
Pass required values into functions.
fn insert_user(db: &sqlite::Connection, name: &str) {
let sql = format!("INSERT INTO users (name) VALUES ('{}')", name);
db.exec(&sql).unwrap();
}
This keeps dependencies obvious.
2. App state struct
Group shared resources together.
struct AppState {
db: sqlite::Connection,
app_name: String,
}
This scales better than adding more globals.
3. Guard clauses and initialization checks
If global state is used, code often checks initialization early.
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn get_config() -> &'static {
CONFIG.().(|s| s.()).()
}
Common Mistakes
1. Assigning a plain value into an Option<T>
Broken code:
static mut DB: Option<sqlite::Connection> = None;
fn main() {
DB = sqlite::open("test.db").expect("open failed");
}
Problem:
DBisOption<sqlite::Connection>- the right side is
sqlite::Connection
Fix:
DB = Some(sqlite::open("test.db").expect("open failed"));
2. Calling methods on Option<T> instead of on T
Broken code:
DB.exec(sql)
Problem:
Comparisons
| Approach | Can hold runtime value? | Mutable? | Safe by default? | Good for DB connection? | Notes |
|---|---|---|---|---|---|
const | No | No | Yes | No | Compile-time only |
static | Yes | No | Yes | Sometimes | Best for immutable global data |
static mut | Yes | Yes | No | Usually no | Requires unsafe |
Cheat Sheet
Quick reference
Immutable global
static NAME: &str = "app";
Mutable global with static mut
static mut VALUE: i32 = 0;
- Requires
unsafeto read or write - Usually avoid in application code
One-time global initialization
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
Safer shared mutable global
use std::sync::{Mutex, OnceLock};
static DATA: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
Important rules
constis for compile-time constantsstaticis for program-lifetime storage
FAQ
Can Rust have global variables?
Yes. Rust supports global data through const, static, and static mut. However, mutable globals are intentionally restricted because they can be unsafe.
Why is static mut discouraged in Rust?
Because it allows shared mutable state, which can lead to data races, aliasing problems, and undefined behavior. Rust prefers safer patterns like borrowing, Mutex, and OnceLock.
Why did my Option<Connection> code fail?
Because you tried to assign a Connection directly into an Option<Connection>, and then called a Connection method on the Option itself. You need Some(connection) and then must unwrap or match the Option.
Should I use a global database connection in Rust?
Usually no. It is more idiomatic to pass &Connection to functions or store the connection inside an app state struct.
What is the idiomatic alternative to global variables in Rust?
Pass values by reference, or group shared resources into a struct such as and pass that struct around.
Mini Project
Description
Build a small Rust program that opens a SQLite database once and passes the connection to helper functions instead of using a global mutable variable. This demonstrates the idiomatic Rust approach to sharing important runtime state across multiple functions.
Goal
Create a SQLite-backed program that initializes a table and inserts data by passing the database connection as a reference.
Requirements
- Open a SQLite database in
main - Pass the connection by reference to at least two helper functions
- Create a table if it does not already exist
- Insert at least one row into the table
- Print a success message for each operation
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Convert an Integer to a String in Rust
Learn the current Rust way to convert integers to strings, why `to_str()` no longer works, and when to use `to_string()` or `format!`.