Why TF does this blog even exist?

Let’s be real — most of us treat pointers like radioactive waste. Let's ignore those dreadful stories of childhood when pointers were introduced to you like a BIG demon. But deep down, the truth is:

Memory is power. And pointers? Pointers are your keys to the throne.

If you’ve ever:

  • Debugged a nasty segfault while chugging a huge load of coffee,
  • Seen your app lag thanks to the garbage collector,
  • Or tried passing huge structs around to make your code modular …

Then you’ve already felt the need for pointers.

This isn’t just a blog. This is a manifesto. We’re gonna break the myths, smash the fear, and teach you how to own memory like an absolute menace — across C (just for understanding how pointers work), Go, and Rust.

By the end of this read, you’ll understand:

  • What pointers really are
  • Why they matter in modern backend systems
  • How Go gives you a chill but powerful pointer experience
  • How Rust forces you into greatness with ownership and borrowing

Let’s cut the fluff and jump right in — welcome to the deep dive.


Basics: Pointers and How They Work (I'm Hoping Y'all Know C)

A pointer just refers to another value. That's it. It is itself a variable that stores the address of another variable(this variable can be a pointer too).
Let's see a quick code snippet in C to recap pointer basics.

#include 

int main() {
    int x = 42;
    int *p = &x; // p points to the address of x

    printf("Value of x: %d\n", x);
    printf("Pointer p: %p\n", p);       // prints address of x
    printf("Value at *p: %d\n", *p);     // dereferencing p, gives 42

    *p = 99; // update x through the pointer
    printf("Updated x: %d\n", x);        // now x is 99

    return 0;
}

Key Concepts :

  • *p → Dereference the pointer (get the value it points to)
  • &x → Address-of operator (get the memory address of x)
  • int *p → Declare a pointer to an integer

Diagram explaining how pointer references work in C

This is where C shows you its true colors: dope performance, minimal overhead — but a single wrong move and it’s undefined behavior land. Manual memory management is both a blessing and a curse.

Let's quickly take a look over other pointer shenanigans in C :

1. Pointers and Arrays:

int arr[3] = {10, 20, 30};
int *ptr = arr; // same as &arr[0]
printf("%d\n", *(ptr + 1)); // prints 20

Arrays decay into pointers — the name of an array is basically a pointer to its first element.

2. Pointer to Pointer:

int x = 5;
int *p = &x;
int **pp = &p;
printf("%d\n", **pp); // double dereference

A pointer to a pointer holds the address of another pointer. Useful in dynamic memory, linked lists, and certain APIs.

3. Function Pointers:

void greet() { printf("Hello!\n"); }
void (*funcPtr)() = greet;
funcPtr(); // calls greet()

Functions live in memory too, and you can point to them! That’s how callbacks and plugin systems are built.

4. Void Pointers:

void *vp;
int a = 7;
vp = &a;
printf("%d\n", *(int *)vp);

Generic pointers that can point to any type. But you need to cast them back before dereferencing.

Wait! There is also pointer arithmetic in C.

In C, pointers aren’t just addresses — they’re math-capable beasts. You can add or subtract integers to pointers to move across array elements (ptr + 1 points to the next element).
But beware: mess up the math, and you're one *(ptr + 9999) away from summoning a segfault demon.(I swear ChatGPT wrote this!)

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;

printf("%d\n", *(ptr + 2)); // prints 3

I just skimmed through the surface of pointers in C. You definitely need to go into a lot more detail. If you want to work with pointers in C, burn this video into your head.

In C, you need to manage memory manually using malloc or calloc and use the pointers very carefully and cautiously. While you have all the control, things can go south easily, even if you miss a little something.

Before diving into how we handle pointer in Go or Rust, let us look at some common concepts in pointers.


Pointer 101

Pointer Declaration

We already saw how to do this in C, and it is pretty easy in Go and Rust too. These code snippets will help you grasp it.

func main() {
    a := 10
    var p *int = &a
    fmt.Println(*p) // dereferencing
}
fn main() {
    let a = 10;
    let p = &a; // immutable reference
    println!("{}", *p);
}

Dynamic Memory Allocation

Even here, the code snippets will help you. The pointers are allocated memory in heap.

int *p = (int*) malloc(sizeof(int));
*p = 42;
p := new(int)
*p = 42
let b = Box::new(42); // heap allocation

Stack vs Heap Allocation in Pointers

When working with pointers in low-level languages like C, Go, and Rust, understanding how memory is allocated — either on the stack or the heap — is non-negotiable if you don’t want your programs to randomly explode (aka segfault).


Stack Allocation

  • Fast and automatically managed.
  • Memory is allocated when a function is called and freed when it returns.
  • Ideal for short-lived variables and function-local data.

C Example:

int main() {
    int x = 42;        // x is on the stack
    int *p = &x;       // p points to stack memory
}

Warning:

Returning a pointer to a stack variable is dangerous. Once the function exits, that memory is gone.

int* bad() {
    int local = 10;
    return &local; // 🚨 Undefined behavior
}

Heap Allocation

  • Slower but gives manual control (malloc/free in C, Box::new in Rust).
  • Data lives until you explicitly free it (or a garbage collector does).
  • Used for long-lived or large data structures.

Stack and Heap Allocation

C Example:

int *p = malloc(sizeof(int));
*p = 99;
free(p);

Rust Example:

let b = Box::new(5); // heap allocation
println!("{}", b);  // Box implements Deref, so it acts like a pointer

Go's Sweet Spot

Go doesn’t let you explicitly allocate on heap or stack — the compiler decides based on escape analysis.

func makePointer() *int {
    x := 10
    return &x // may be heap-allocated if it escapes
}

TL;DR

Feature Stack Heap
Lifetime Until function returns Until explicitly freed
Speed Fast Slower
Managed by Compiler Developer / GC
Use case Small, short-lived data Large, persistent data
Typical Bug Use-after-return Memory leaks, double free

Stack = speed.
Heap = flexibility.
Misunderstanding either = pain.


Pass By Value v/s Pass By Reference

What is Pass-by-Value?

  • A copy of the variable is passed to the function.
  • The original variable does not change.

What is Pass-by-Reference?

  • A reference or pointer is passed.
  • The function can modify the original variable. Pass by value v/s Pass by reference

Pass-by-Value (default in C)

void modify(int a) {
    a = 100;
}

int main() {
    int x = 10;
    modify(x);
    printf("%d\n", x); // prints 10
}

Pass-by-Reference using Pointer

void modify(int *a) {
    *a = 100;
}

int main() {
    int x = 10;
    modify(&x);
    printf("%d\n", x); // prints 100
}

Pass-by-Value (default in Go)

func modify(a int) {
    a = 100
}

func main() {
    x := 10
    modify(x)
    fmt.Println(x) // prints 10
}

Pass-by-Reference using Pointers

func modify(a *int) {
    *a = 100
}

func main() {
    x := 10
    modify(&x)
    fmt.Println(x) // prints 100
}

Even slices, maps, channels in Go are passed by value, but since the value contains an internal pointer, they often behave like references.

Rust always passes by value, but with borrowing (&) or mutable borrowing (&mut), you simulate reference behavior safely.

Pass-by-Value (moves or copies the value)

fn modify(mut a: i32) {
    a = 100;
}

fn main() {
    let x = 10;
    modify(x);
    println!("{}", x); // prints 10
}

Pass-by-Reference (Borrowing)

fn modify(a: &mut i32) {
    *a = 100;
}

fn main() {
    let mut x = 10;
    modify(&mut x);
    println!("{}", x); // prints 100
}

Rust ensures no data races or dangling pointers using its ownership model and borrow checker. (We will understand more of this later)


Side-by-Side Summary

Language Default Behavior Reference Support Mutable Ref Support Safety Mechanism
C Pass-by-Value ✅ Yes (manual *) ✅ Yes ❌ Manual, unsafe
Go Pass-by-Value ✅ Yes (with *T) ✅ Yes ❌ Garbage-collected
Rust Pass-by-Value ✅ Yes (&T) ✅ Yes (&mut T) ✅ Ownership & Borrowing

Key Facts and Gotchas — Pointers in C, Go, and Rust

C

You can:

  • Do pointer arithmetic
  • Use null pointers
  • Cast between pointer types
  • Create pointer-to-pointer
  • Manually manage heap (malloc/free)
  • Point to functions

You must handle:

  • No safety — undefined behavior is easy
  • Manual memory management (malloc/free)
  • Dangling pointers, memory leaks, buffer overflows
  • Segfaults due to wild pointers

Go

You can:

  • Use pointers to access and modify values
  • Pass pointers to functions
  • Allocate memory with new() or make() (for slices, maps, etc.)

But you cannot:

  • Do pointer arithmetic
  • Create pointer-to-pointer (**int)
  • Manually free memory (garbage collected)
  • Have nil-check safety for all types (you can nil dereference)
  • Take pointer to literal (p := &10 is invalid)

🔹 Example:

arr := []int{1, 2, 3}
// Can't do: ptr := &arr[0] + 1  ❌
// Use indexing instead
fmt.Println(arr[1])

As said earlier, Go passes structs/slices by value, but since slices contain an internal pointer, they behave “reference-like.”

See :

func modify(s []int) {
    s[0] = 999
}

func main() {
    a := []int{1, 2, 3}
    modify(a)
    fmt.Println(a) // [999 2 3] - gotcha!
}

Rust

You can:

  • Use references (&T, &mut T) safely
  • Use Box, Rc, Arc for heap allocation and shared ownership
  • Use raw pointers in unsafe blocks (*const, *mut)
  • Use Option instead of null

You cannot:

  • Have null pointers (None is used instead)
  • Mutably borrow more than once simultaneously (borrow checker)
  • Dereference or do pointer arithmetic outside unsafe
  • Leak memory unless you std::mem::forget or Box::leak

Example:

let x = Some(10);
let y: Option<&i32> = None; // instead of a null pointer
let a = [1, 2, 3];
let p = a.as_ptr();
unsafe {
    println!("{}", *p.add(1)); // Pointer arithmetic in unsafe block
}

TL;DR: Cheat Sheet

Feature / Language C Go Rust
Null Pointers NULL nil ❌ Use Option
Pointer Arithmetic ✅ Yes ❌ No ✅ Yes (unsafe only)
Pointer-to-Pointer ✅ Yes ❌ No ✅ Yes (&&T)
Automatic Memory Mgmt ❌ No ✅ GC ✅ Ownership model
Dangling Pointer Safety ❌ No ❌ No ✅ Checked at compile
Struct Ref Mutability ✅ Manual ✅ Pointers ✅ Borrow system
Ref Counted Shared Ptr ❌ Manual ✅ Maps etc. Rc, Arc
Manual Free ✅ Yes ❌ No Drop trait
Function Pointers ✅ Yes ✅ Yes fn, Fn, FnMut

Garbage Collection in Go v/s Ownership in Rust

Choose your weapon: Lazy janitor vs Memory tyrant.


Go's Garbage Collection (GC) — The Chill Roommate

Go takes a comfy, high-level approach. You allocate memory, use it, and forget it — the Go runtime’s GC cleans it up like a silent ninja.

How it Works:

  • Go uses a concurrent, tri-color mark-and-sweep garbage collector.
  • It pauses briefly, walks through live objects, marks them, and frees unmarked (unreachable) memory.
  • GC runs in the backgroundyou don’t control it directly.

Example:

type User struct {
    name string
}

func main() {
    u := &User{name: "pixie"}
    fmt.Println(u.name) // GC takes care of `u` when it's no longer used
}

Pros:

  • Zero manual memory management.
  • Great for productivity, safety, and fast dev cycles.
  • No need to think about free() or memory leaks most of the time.

Cons:

  • GC pauses = potential latency spikes.
  • GC can kick in at random times = non-deterministic behavior.
  • Less control over performance-sensitive workloads (e.g., real-time systems).

GC is like Uber Eats: you chill, your trash gets picked up eventually — but not always when you want it.


Rust’s Ownership Model — The Memory Warlord’s Code

Rust said:

“Why GC when you can just never make a memory mistake?”

Instead of garbage collection, Rust uses ownership + lifetimes + borrowing to statically guarantee memory safety — at compile time.

Core Concepts:

  • Ownership: Every value has a single owner.
  • Borrowing: You can lend references without transferring ownership.
  • Lifetimes: Ensures references are valid as long as they're used.
fn main() {
    let s = String::from("hello"); // s owns the String
    takes_ownership(s);            // ownership moved, s is now invalid
    // println!("{}", s);          // ❌ Compile error

    let x = 5;
    makes_copy(x);                 // i32 implements Copy, x still valid
}

fn takes_ownership(s: String) {
    println!("{}", s);
}

fn makes_copy(x: i32) {
    println!("{}", x);
}

Pros:

  • Zero runtime cost (no GC!)
  • Compile-time guarantees = no segfaults, no leaks, no data races.
  • Absolute control — systems level performance, with high-level safety.

Cons:

  • Steeper learning curve (borrow checker will roast you alive at first).
  • More verbose code with &, &mut, lifetimes.
  • Need to think deeply about lifetimes and ownership flows.

Rust is like being Batman: no sidekicks, no help — just you and your gear. But once you master it? You're unstoppable.


TL;DR — Go GC vs Rust Ownership

Feature Go (Garbage Collected) Rust (Ownership Model)
Memory Management Automatic via GC Compile-time via Ownership
Control Minimal Complete
Performance Overhead GC pauses, unpredictable latency Near-zero runtime cost
Memory Safety Mostly safe, but nil pointers exist Guaranteed via borrow checker
Ease of Use Easy, chill Steep learning curve, then power
Real-time Systems ❌ Not ideal ✅ Excellent fit

Some Wisdom Drop

  • If you want developer productivity with decent performance: Go.
  • If you want maximum performance with zero runtime overhead and are willing to tame the beast: Rust.
  • Want to be a backend god-tier villain? Learn both. Use the right tool for the right kill.

Chaos to Calm - Pointers in Go

So now that you've stared into the void (pointer pun intended) with C — raw, dangerous, and obviously a double edged sword — let's take a deep breath and walk into a language that hands you pointers… but without the existential crisis.

Go doesn’t give you pointer arithmetic. It won’t let you shoot yourself in the foot (most of the time). But it does give you:

  1. Direct memory referencing with & and *
  2. Lightweight object sharing via pointer receivers
  3. Safe heap allocation using new()
  4. And just enough low-level vibes to keep your inner villain happy

By the above examples , it must be pretty clear how pointers work in go, let's jump to working with structs and pointers in go.


Pointers and Structs in Go

Structs in Go are value types by default. This means assigning a struct or passing it to a function copies the whole thing — which could suck for performance.

When to Use Pointers with Structs

  • When your struct is large and copying is expensive
  • When you want to mutate the original struct in a function
  • When you want to avoid frequent allocations in tight loops
type Villain struct {
    Name  string
    PowerLevel int
}

func boost(v *Villain) {
    v.PowerLevel += 100
}

func main() {
    v := Villain{"Yashaswi", 9000}
    boost(&v)
    fmt.Println(v.PowerLevel) // 9100
}

When NOT to Use Pointers

  • If your struct is small (e.g., a few fields) and used immutably
  • If you don’t need to share/mutate state across functions
  • If you're creating short-lived structs inside a loop — prefer values to help stack allocation

Rule of Thumb

  • If it's less than or equal to 3 words (e.g., 24 bytes), pass by value.
  • If you need shared/mutable state, use pointers.
  • For methods, use pointer receivers if the method needs to mutate or avoid copying.
type Engine struct {
    HorsePower int
}

func (e Engine) PrintHP() { // value receiver
    fmt.Println(e.HorsePower)
}

func (e *Engine) Boost() { // pointer receiver
    e.HorsePower += 100
}

Memory Best Practices in Go

Avoid unnecessary allocations

  • Use value receivers where mutation isn't needed.
  • Preallocate slices and maps where possible.

Escape analysis matters

  • If a variable escapes to the heap (vs staying on the stack), it increases GC pressure.
  • Use go build -gcflags=-m to check if variables escape.

Use sync.Pool for reusable memory

  • Great for reducing GC load in high-performance apps.

Don't fear pointers

  • They’re safe, controlled, and let you avoid copying large structs.

Mind the zero values

  • In Go, everything has a zero value. Don’t waste allocations initializing something that’s already zero. ### ⚠️ What Go Doesn’t Let You Do (And That’s Okay)
    • No manual malloc/free.
    • No pointer arithmetic.
    • No direct memory manipulation (you’ll need unsafe for that).

But hey, that’s the point — Go protects you from footguns so you can ship faster without being reckless. You're still working with memory, just with guardrails.


That's a wrap

And there you have it — the journey from raw pointers to Go vs. Rust. We’ve covered how memory is managed at its most basic level, the stack vs. heap debate, and the power (and peril) of manual memory management.

In the world of Rust, we’ve seen how the ownership system isn’t just a feature — it’s a whole new way to think about memory safety. With borrowing and lifetimes, Rust enforces a discipline that forces you to think ahead, leaving no room for memory leaks or dangling pointers. It’s a challenge, but one that builds robust, zero-cost abstractions.

In Go, we saw how things are more laid-back with garbage collection and pointers that you only really need when performance or interop is key. Go’s approach has its benefits, but it doesn’t give you the same fine-grained control as Rust.

Remember: pointers are a tool. Whether you’re dealing with them in Go or Rust, you control the power — but with great power comes great responsibility. Master these concepts, and you’ll be writing software that’s not just efficient, but invincible.

Also, we’ve only scratched the surface with Rust. In the coming one, we’ll dive into the real villain territory of Rust’s ownership, smart pointers, lifetimes, threads, and channels. Are you ready to level up your backend skills?

Until then, keep your pointers sharp and your memory clean.

Bye Bye