Back to Blog
C vs Rust vs Go: Systems Programming Language Comparison

C vs Rust vs Go: Systems Programming Language Comparison

December 18, 2024
7 min read
Tushar Agrawal

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 <stdio.h>
#include <stdlib.h>
#include <string.h>

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 main

import "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 <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#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<Vec<i32>>, start: usize, end: usize) -> i64 {
    data[start..end].iter().map(|&x| x as i64).sum()
}

#[tokio::main]
async fn main() {
    let data: Arc<Vec<i32>> = 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 main

import (
    "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<String, io::Error> {
    fs::read_to_string(path)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    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
Choose based on your project's safety requirements, performance needs, and team expertise. Many organizations use multiple languages, selecting the right tool for each component.

Related Articles

Share this article

Related Articles