C vs Rust vs Go: Systems Programming Language Comparison
Compare C, Rust, and Go for systems programming. Analyze memory safety, performance, concurrency, and use cases to choose the right language for your project.
Introduction
Systems programming requires languages that provide control over hardware resources while maintaining performance. C has dominated this space for 50 years, but Rust and Go offer modern alternatives with different trade-offs. C provides raw power, Rust guarantees memory safety without garbage collection, and Go prioritizes simplicity and fast development.
Language Philosophy
C: Maximum Control
// C - Direct memory manipulation
#include
#include
#include typedef struct {
int id;
char name[50];
double balance;
} Account;
Account create_account(int id, const char name, double balance) {
Account acc = (Account)malloc(sizeof(Account));
if (acc == NULL) return NULL;
acc->id = id;
strncpy(acc->name, name, 49);
acc->name[49] = '\0';
acc->balance = balance;
return acc;
}
void free_account(Account* acc) {
free(acc);
}
// Manual memory management - no safety guarantees
int main() {
Account* acc = create_account(1, "John Doe", 1000.0);
if (acc) {
printf("Account: %s, Balance: %.2f\n", acc->name, acc->balance);
free_account(acc);
}
// Danger: use-after-free possible if acc is accessed here
return 0;
}
Rust: Safety Without Sacrifice
// Rust - Memory safety at compile time
struct Account {
id: u32,
name: String,
balance: f64,
}impl Account {
fn new(id: u32, name: &str, balance: f64) -> Self {
Account {
id,
name: name.to_string(),
balance,
}
}
}
fn main() {
let acc = Account::new(1, "John Doe", 1000.0);
println!("Account: {}, Balance: {:.2}", acc.name, acc.balance);
// acc is automatically freed when it goes out of scope
// Use-after-free is impossible - compiler prevents it
}
// Ownership and borrowing
fn process_accounts(accounts: &[Account]) {
for acc in accounts {
println!("{}: {}", acc.name, acc.balance);
}
// accounts is borrowed, not moved - original owner keeps it
}
Go: Simplicity and Productivity
// Go - Simple, garbage collected
package mainimport "fmt"
type Account struct {
ID int
Name string
Balance float64
}
func NewAccount(id int, name string, balance float64) *Account {
return &Account{
ID: id,
Name: name,
Balance: balance,
}
}
func main() {
acc := NewAccount(1, "John Doe", 1000.0)
fmt.Printf("Account: %s, Balance: %.2f\n", acc.Name, acc.Balance)
// Garbage collector handles memory
}
Memory Management
Memory Management Comparison
============================Aspect C Rust Go
──────────────────────────────────────────────────────────────
Model Manual Ownership Garbage Collection
Safety None (runtime) Compile-time Runtime (GC)
Performance Best Near-C Good (GC pauses)
Memory Leaks Possible Prevented Prevented
Dangling Ptrs Possible Prevented Prevented
Data Races Possible Prevented Possible (goroutines)
Learning Curve Moderate Steep Easy
Rust Ownership Example
// Rust ownership prevents memory issues at compile time
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2 // println!("{}", s1); // Compile error! s1 is no longer valid
// Borrowing allows multiple readers
let s3 = String::from("world");
let len = calculate_length(&s3); // Borrow s3
println!("{} has length {}", s3, len); // s3 still valid
// Mutable borrow
let mut s4 = String::from("hello");
change(&mut s4);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
fn change(s: &mut String) {
s.push_str(", world");
}
Performance Benchmarks
Benchmark Results (Relative Performance)
========================================Task C Rust Go
────────────────────────────────────────────
Binary search 1.0x 1.0x 1.5x
HTTP server 1.0x 1.05x 1.3x
JSON parsing 1.0x 1.1x 2.0x
Regex matching 1.0x 0.95x 3.0x
Memory allocation 1.0x 1.0x 2.0x
Concurrent tasks 1.0x 1.0x 0.9x
Binary Size (Hello World)
─────────────────────────
C: 16KB
Rust: 300KB (default), 50KB (optimized)
Go: 2MB (static), 1MB (stripped)
Compile Time (100K LOC)
───────────────────────
C: 10s
Rust: 60s
Go: 5s
Concurrency
C with pthreads
#include
#include
#include #define NUM_THREADS 4
typedef struct {
int* data;
int start;
int end;
long result;
} ThreadArgs;
void sum_array(void args) {
ThreadArgs ta = (ThreadArgs)args;
ta->result = 0;
for (int i = ta->start; i < ta->end; i++) {
ta->result += ta->data[i];
}
return NULL;
}
int main() {
int data[1000];
for (int i = 0; i < 1000; i++) data[i] = i;
pthread_t threads[NUM_THREADS];
ThreadArgs args[NUM_THREADS];
int chunk = 1000 / NUM_THREADS;
for (int i = 0; i < NUM_THREADS; i++) {
args[i].data = data;
args[i].start = i * chunk;
args[i].end = (i + 1) * chunk;
pthread_create(&threads[i], NULL, sum_array, &args[i]);
}
long total = 0;
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
total += args[i].result;
}
printf("Total: %ld\n", total);
return 0;
}
Rust with async/await
use tokio;
use std::sync::Arc;async fn sum_chunk(data: Arc>, start: usize, end: usize) -> i64 {
data[start..end].iter().map(|&x| x as i64).sum()
}
#[tokio::main]
async fn main() {
let data: Arc> = Arc::new((0..1000).collect());
let chunk_size = 250;
let handles: Vec<_> = (0..4)
.map(|i| {
let data = Arc::clone(&data);
let start = i * chunk_size;
let end = start + chunk_size;
tokio::spawn(async move { sum_chunk(data, start, end).await })
})
.collect();
let total: i64 = futures::future::join_all(handles)
.await
.into_iter()
.map(|r| r.unwrap())
.sum();
println!("Total: {}", total);
}
// Rust prevents data races at compile time
// This won't compile:
// fn data_race() {
// let mut data = vec![1, 2, 3];
// std::thread::spawn(|| data.push(4)); // Error: data moved
// data.push(5); // Error: data already moved
// }
Go with goroutines
package mainimport (
"fmt"
"sync"
)
func sumChunk(data []int, result chan<- int64, wg *sync.WaitGroup) {
defer wg.Done()
var sum int64
for _, v := range data {
sum += int64(v)
}
result <- sum
}
func main() {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
results := make(chan int64, 4)
var wg sync.WaitGroup
chunkSize := 250
for i := 0; i < 4; i++ {
wg.Add(1)
start := i * chunkSize
end := start + chunkSize
go sumChunk(data[start:end], results, &wg)
}
go func() {
wg.Wait()
close(results)
}()
var total int64
for sum := range results {
total += sum
}
fmt.Printf("Total: %d\n", total)
}
Error Handling
// C - Return codes
int read_file(const char path, char* content) {
FILE* f = fopen(path, "r");
if (!f) return -1; fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
*content = malloc(size + 1);
if (!*content) {
fclose(f);
return -2;
}
fread(*content, 1, size, f);
(*content)[size] = '\0';
fclose(f);
return 0;
}
// Rust - Result type
use std::fs;
use std::io;fn read_file(path: &str) -> Result {
fs::read_to_string(path)
}
fn main() -> Result<(), Box> {
let content = read_file("file.txt")?; // ? propagates errors
println!("{}", content);
Ok(())
}
// Go - Multiple returns
func readFile(path string) (string, error) {
content, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("reading %s: %w", path, err)
}
return string(content), nil
}
Use Cases
When to Use Each Language
=========================Use C when:
├── Writing OS kernels or drivers
├── Embedded systems with tight constraints
├── Need absolute performance control
├── Interfacing with legacy C code
└── Maximum portability required
Use Rust when:
├── Memory safety is critical
├── Building concurrent systems
├── Web browsers, game engines
├── CLI tools with native performance
└── Replacing C in new projects
Use Go when:
├── Building web services and APIs
├── DevOps and infrastructure tools
├── Need fast development cycles
├── Team productivity matters
└── Kubernetes/cloud-native apps
Decision Matrix
Language Selection Matrix
=========================Criteria C Rust Go
──────────────────────────────────────────────
Memory safety ✗ ✓✓✓ ✓✓
Raw performance ✓✓✓ ✓✓✓ ✓✓
Concurrency safety ✗ ✓✓✓ ✓✓
Development speed ✓ ✓✓ ✓✓✓
Learning curve ✓✓ ✓ ✓✓✓
Ecosystem maturity ✓✓✓ ✓✓ ✓✓✓
Binary size ✓✓✓ ✓✓ ✓
Compile time ✓✓ ✓ ✓✓✓
Cross-compilation ✓✓ ✓✓✓ ✓✓✓
✓✓✓ = Excellent ✓✓ = Good ✓ = Adequate ✗ = Poor
Conclusion
- C: Maximum control, best for OS/embedded, requires expertise
- Rust: Modern C replacement with safety guarantees, steeper learning curve
- Go: Productive systems programming, excellent for services and tools
Related Articles
- Go vs Java Backend Comparison - Compare Go with Java
- Python vs Go Backend Comparison - Go vs Python
- System Design Interview Guide - Architecture patterns
- Docker & Kubernetes Deployment Guide - Deploy your applications