Jump to content

Seven Things Go Lets You Do That Rust Won’t (By Design)

From JOHNWICK

I once attempted to incorporate a small background task into a standard HTTP handler. In Go, I pushed a value to a channel, returned 202, and moved on. In Rust, I had to choose an executor, mark functions async, and prove who owned the state I wanted to touch. Neither language was “wrong.” They were simply forcing different habits. This article is a map of those defaults—and the seven places where Go lets you act first while Rust makes you prove it first. Why This Exists Both languages are excellent. You can build almost anything in either. The point here is ergonomics under a deadline. Go ships a runtime and batteries that smooth common backend work. Rust keeps the core small and pushes you to design ownership, lifetimes, and concurrency with precision. That design choice affects your calendar, your incidents, and your team’s mood. How The Concurrency Feels Go (Preemptive Runtime) [Goroutine A runs] --runtime may preempt--> [Goroutine B runs] --> [A resumes]

Rust (Cooperative Async) [Task A] --- work --- .await (explicit yield) -- resume --> .await --> [Task B] ---------------------- runs only when someone awaits ---------------------- Seven Design Choices Where Go Moves Faster 1) Start Work From Anywhere, Without Leaking Types Go lets you launch work from synchronous code with go f(), keep your handler signature, and rely on the scheduler.
Rust makes async explicit: async fn, Future returns, and an executor in the picture. That ceremony prevents accidental blocking and clarifies flow, but it slows casual “just do this in the background” changes. 2) Share Mutable State Quickly — Or Prove Safety First Go gives you sync.Mutex, sync.Map, and channels. You can move fast and use the race detector plus reviews.
Rust forbids data races in safe code. To share mutation, you reach for Arc<Mutex<T>> (or similar) and satisfy the borrow checker. It’s friction with purpose. 3) Ship A Web Service From The Standard Library Go’s net/http, database/sql, crypto/*, encoding/json cover a lot of ground. You can deliver a non-trivial service with zero third-party code.
Rust’s standard library is intentionally slim; the community crates are superb (Axum/Hyper/Serde), but you choose them, wire them, and pick a runtime. 4) Tens Of Thousands Of Tasks, Built-In Scheduler Goroutines are cheap, and the runtime multiplexes them on OS threads for you. The “one goroutine per connection” habit feels natural.
Rust can scale similarly, but the scheduling strategy lives in the executors you select. 5) Channels And select Are Language Features In Go, channels are first-class and select is a keyword. Fan-in, fan-out, and worker pools feel baked in.
In Rust, channels are libraries (mpsc, tokio::sync, crossbeam). Powerful and typed, but not language primitives. 6) One Static Binary With Minimal Ceremony go build often yields a portable, static-ish binary. Cross-compile with a couple of env vars.
Rust also emits a single binary; fully static builds and native deps can require extra target setup. Not hard—just more knobs. 7) Prototype First, Then Design Go invites a one-file start, go fmt, and iteration.
Rust rewards you for modeling ownership on day one. You’ll think harder up front—especially for self-referential structures or cycles—and you’ll pay back that time in fewer foot-guns later.


Side-By-Side: A Tiny Server With Background Work Go (Stdlib Only, One Binary) package main

import (

"fmt"
"net/http"
"time"

)

func main() {

jobs := make(chan int, 8)
go func() {
 for j := range jobs {
  time.Sleep(50 * time.Millisecond) // pretend work
  fmt.Println("done job", j)
 }
}()
http.HandleFunc("/do", func(w http.ResponseWriter, r *http.Request) {
 select {
 case jobs <- time.Now().Nanosecond():
  w.WriteHeader(http.StatusAccepted)
  w.Write([]byte("queued\n"))
 default:
  http.Error(w, "busy", http.StatusTooManyRequests)
 }
})
http.ListenAndServe(":8080", nil)

} Rust (Async Runtime And Typed Channel) use axum::{routing::get, Router}; use std::net::SocketAddr; use tokio::sync::mpsc;

  1. [tokio::main]

async fn main() {

   let (tx, mut rx) = mpsc::channel::<u64>(8);
   tokio::spawn(async move {
       while let Some(job) = rx.recv().await {
           tokio::time::sleep(std::time::Duration::from_millis(50)).await;
           println!("done job {job}");
       }
   });
   let app = Router::new().route("/do", get({
       let tx = tx.clone();
       move || {
           let tx = tx.clone();
           async move {
               tx.send(42).await.map(|_| "queued\n").map_err(|_| "busy\n")
           }
       }
   }));
   let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
   axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();

} What To Notice: Go hides the runtime; Rust makes it explicit. Go’s handler stays synchronous; Rust’s handler is an async future. Both are clear, just with different defaults.


Try A Micro-Experiment At Home You do not need synthetic charts to learn something real. Here’s a simple, fair test you and your readers can run and discuss:

  • Build each server in release mode on the same machine.
  • Warm each for 10–15 seconds.
  • From another terminal, hit /do with a load tool for ~20 seconds at moderate concurrency.
  • Capture throughput and CPU, then repeat with higher concurrency.
  • Post your numbers and what surprised you. Did preemption vs awaits matter? Did backpressure show up?

This turns the comments into a shared lab, not a shouting match. A Short, Honest Decision Box Reach For Go When: - You want stdlib HTTP/TLS/JSON and one obvious path to prod - You need to “start a background thing now” without refactoring types - Team prefers runtime help over compile-time proofs

Reach For Rust When: - Data races in safe code are unacceptable - You need tight control, zero-cost abstractions, and predictable latency - Service will live for years, and correctness debt is your enemy


Final Word And A Challenge Go’s defaults let you move without asking permission. Rust’s defaults make you state your intent and prove safety. Both are gifts — just aimed at different risks. Now I want your perspective:

  • Which of the seven would you remove, and what would you replace it with?
  • Share your micro-experiment results and the code you used. I’ll read every comment and publish a follow-up that features the best counterexamples and lessons from the thread.

If this helped you frame a team decision, say hello below. Your story helps other engineers who are choosing under pressure.

Read the full article here: https://medium.com/@samurai.stateless.coder/seven-things-go-lets-you-do-that-rust-wont-by-design-1652127a23ff