Jump to content

7 Rust Concurrency Patterns Every Go Dev Should Steal

From JOHNWICK
Revision as of 08:14, 19 November 2025 by PC (talk | contribs) (Created page with "500px You think Go is the concurrency language until you ship something that melts under real pressure. Not hello-world pressure.
Not “5 goroutines in localhost” pressure.
Real traffic. Real money. Real users who do not refresh, they uninstall. That is when you stop asking “can I spawn more goroutines?” and start asking “what exactly is touching this memory, and who’s allowed to touch it?” Go shrugs. Rust answer...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

You think Go is the concurrency language until you ship something that melts under real pressure. Not hello-world pressure.
Not “5 goroutines in localhost” pressure.
Real traffic. Real money. Real users who do not refresh, they uninstall. That is when you stop asking “can I spawn more goroutines?” and start asking “what exactly is touching this memory, and who’s allowed to touch it?” Go shrugs. Rust answers. This is the part Go developers should steal. 1. Ownership shuts down data races before you run the code In Go you can write this and it compiles just fine: // Two goroutines mutating the same slice. items := []int{} go func() { items = append(items, 1) }() go func() { items = append(items, 2) }() You just crossed your fingers and hoped the scheduler is kind today. In Rust, the compiler slaps you immediately if two threads can mutate the same data without coordination. use std::thread;

let mut items = Vec::new(); thread::spawn(|| {

   // cannot borrow `items` as mutable
   // more than once at a time
   items.push(1);

}); This does not even build. That sounds annoying until you realize what actually happened:

  • You prevented a heisenbug at compile time.
  • You prevented a production outage at compile time.
  • You prevented a 3 a.m. blame call at compile time.

Ownership is not a style. It is a guarantee.
Steal the mindset: “Who owns this data right now? And who is not allowed to?” If your Go code cannot answer that cleanly, that is a red flag. 2. Send data, not access to data Here is the biggest mental model shift. In Go, you often share one big struct and protect it with a mutex. In Rust, you are pushed to move data to the worker that needs it instead of letting everyone poke the same thing. Visually: Go style: +----------+ | shared | | state | <--- lock around it +----------+

  ^    ^
  |    |
gor1 gor2

Rust style: +-------+ +-------+ | dataA | --> | T1 | +-------+ +-------+

+-------+ +-------+ | dataB | --> | T2 | +-------+ +-------+ You do not ship pointers everywhere.
You ship ownership. Why this matters: contention drops.
Your p95 stops spiking when load jumps.
Your locks do not become single-lane traffic. You can copy this in Go today: instead of “give workers access to shared map,” try “give each worker its own slice of work and let them own it fully.” 3. Channels with types that cannot lie Go channels are great, but they will happily send anything that compiles, even if “anything” is the wrong thing. Rust channels are strongly typed, and they tie into ownership. When you send a value through a channel, you move it. The sender no longer owns it. use std::sync::mpsc; use std::thread;

let (tx, rx) = mpsc::channel(); let worker = thread::spawn(move || {

   let job = rx.recv().unwrap();
   // worker owns `job` now

}); tx.send("resize-image-job".to_string()).unwrap(); // tx cannot touch that String anymore Why steal this?

  • After send, the producer literally cannot mutate that job.
  • The worker is the only owner.
  • No hidden alias. No sneaky write after dispatch.

In Go, producer and consumer can both still access the object after send. That is a silent foot gun. When you design Go code, pretend send() transfers soul, not just value. Producer should act like it no longer owns it. 4. Arc + Mutex instead of “hope nobody abuses this global” In Go: var cache = map[string]string{} var mu sync.Mutex

func put(k, v string) {

   mu.Lock()
   cache[k] = v
   mu.Unlock()

} Everyone sees cache. Anyone can grab it by mistake. Anyone can read without the lock “just for a quick debug.” Rust forces you to wrap shared mutable state in explicit concurrency primitives: Arc<Mutex<T>>. use std::sync::{Arc, Mutex}; use std::thread;

let cache = Arc::new(Mutex::new(std::collections::HashMap::new())); let cache2 = Arc::clone(&cache); let t = thread::spawn(move || {

   let mut m = cache2.lock().unwrap();
   m.insert("key".into(), "val".into());

}); t.join().unwrap(); // cannot touch `m` here, lock guard is gone Two things to steal for Go:

  • Do not expose raw globals. Expose access functions only.
  • Make it impossible to hold the lock longer than needed.

In Rust, the lock guard dies at scope end.
In Go, people forget Unlock() in error paths and production starts cooking. 5. Scoped lifetimes instead of “who still references this?” Rust refuses to let you return references to data that is about to die. That sounds academic. It is not. Dangling references in concurrent systems are horror. Imagine you spawn workers that all keep a pointer to a buffer that already got freed. With Go’s garbage collector, you almost never see this exact pattern, but you absolutely see something similar: goroutines still reading configs, clients, db handles that you meant to rotate or close. Rust forces you to prove lifetimes line up.
Your worker cannot outlive the thing it borrows. In Go, you have to enforce this by contract. Usually in comments. Which people do not read. Steal this: when you spawn goroutines, ask “what are they closing over, and when does that expire?” If you do not know, that is a production leak in the making. 6. Immutable by default, mutation is special In Rust, values are immutable unless you mark them mut.
In Go, everything is mutable unless you show discipline. Sounds small. It is not small under load. Immutable data means you can safely share read-only snapshots between threads with zero locks and zero drama. That is huge for read-heavy systems, caches, config, feature flags, lookup tables. Pattern to steal in Go: treat configs and lookup maps as write-once immutable snapshots. When you “update config,” build a brand new snapshot and atomically swap the pointer for everyone. Never mutate the live one in place. This alone kills so many weird “goroutine saw half-written state” bugs. 7. Fearless parallelism, not reckless parallelism Go made concurrency feel cheap.
That was the gift and the trap. Rust makes concurrency feel serious.
You do not spawn 200 workers because “why not.”
You design who owns what, who shuts down what, and who is allowed to mutate what. The result is boring graphs:

  • flat CPU, not sawtooth chaos
  • steady p99, not random 2s spikes
  • zero “I swear this only happens in prod” tickets

That is what you actually want. Not “fast.”
Predictable. Steal the discipline, keep your language You do not have to rewrite your Go service in Rust tomorrow. But you should steal these seven things today:

  • Treat shared state like radioactive material.
  • Move data instead of sharing it.
  • Assume send() gives up ownership.
  • Lock in tiny scopes.
  • Never let a goroutine outlive what it depends on.
  • Prefer immutable snapshots.
  • Stop calling “more goroutines” a scaling strategy.

Go made concurrency easy. Rust makes it correct. You want both.