A recent piece at hamy.xyz makes a pragmatic case: you can write Rust as though it were a higher-level language, leaning on clones, owned types, and runtime reference counting rather than fighting the borrow checker at every turn. This gives you most of the language’s safety benefits with a fraction of the cognitive overhead.
That framing is useful, but it leaves a question open: what, specifically, do you keep and what do you give up? The answer matters because the tradeoffs are not symmetric. Some of what you give up is meaningful. Most of it is not, for the majority of applications.
What the Borrow Checker Still Protects
When people complain about Rust’s ownership system, they’re usually talking about lifetime annotations and the situations where the borrow checker refuses to compile code that seems obviously safe. That’s the pain the high-level approach largely sidesteps.
What it does not sidestep is the underlying guarantee: Rust’s type system still prevents data races at compile time regardless of how many clones you use. If you wrap shared state in Arc<Mutex<T>> and clone the Arc freely, the compiler still enforces that you hold the lock before accessing the inner value. If you forget to acquire the mutex, the code does not compile. That is a fundamentally different situation from Go, where the race detector is a runtime tool and data races compile without complaint.
The memory safety story is the same. Even if you never write a single lifetime annotation and clone strings at every opportunity, Rust still prevents use-after-free, double-free, and null pointer dereferences. These are not runtime checks. They are compile-time guarantees, and they hold regardless of how high-level your code looks.
This distinction is easy to understate. The class of bugs that Rust eliminates at compile time, including buffer overreads, dangling pointers, and concurrent mutation, are the bugs that cause the majority of CVEs in systems written in C and C++. The memory safety statistics from Microsoft and Google consistently put memory safety bugs at around 70% of serious security vulnerabilities in their codebases. High-level Rust closes that door just as firmly as zero-copy Rust does.
The Concrete Pattern Stack
High-level Rust converges on a small set of patterns that handle almost every situation without requiring deep ownership expertise.
Owned types over borrowed types. Use String instead of &str, Vec<T> instead of &[T], PathBuf instead of &Path. Borrowed types are more efficient but require lifetime annotations the moment they cross function boundaries in non-trivial ways. Owned types clone the data and sidestep the problem entirely.
Clone early and often. Calling .clone() is not a design failure. For most application-level code, the clone is cheap relative to I/O, network latency, or serialization. The ergonomic cost of fighting the borrow checker typically outweighs the computational cost of an extra heap allocation.
Arc<Mutex<T>> for shared state. When you need to share mutable state across threads or async tasks, Arc<Mutex<T>> is the standard pattern. It has a runtime cost: the reference count update is atomic and the mutex acquisition has overhead. For shared application state accessed a few hundred times per second, that cost is irrelevant.
anyhow for error handling. The anyhow crate gives you a single anyhow::Error type that any standard error can be converted into automatically. Instead of threading error type parameters through every function signature, you write Result<T, anyhow::Error> or the shorthand anyhow::Result<T>. This is roughly equivalent to Go’s error interface in ergonomics, without giving up Rust’s structured error handling when you need it.
Concretely, a high-level Rust function that reads a config file and parses it might look like this:
use anyhow::Result;
use std::fs;
#[derive(Debug, Clone, serde::Deserialize)]
struct Config {
database_url: String,
max_connections: u32,
}
async fn load_config(path: &str) -> Result<Config> {
let contents = fs::read_to_string(path)?;
let config: Config = toml::from_str(&contents)?;
Ok(config)
}
No lifetime annotations. No custom error types. The ? operator propagates errors up the call stack automatically, converting any std::error::Error implementor into anyhow::Error on the way. This reads more like Python with types than like the Rust you see in embedded systems tutorials.
For shared state across async tasks, the pattern is equally mechanical:
use std::sync::Arc;
use tokio::sync::Mutex;
use std::collections::HashMap;
type SharedCache = Arc<Mutex<HashMap<String, String>>>;
async fn get_or_fetch(cache: SharedCache, key: String) -> anyhow::Result<String> {
{
let guard = cache.lock().await;
if let Some(val) = guard.get(&key) {
return Ok(val.clone());
}
}
let value = fetch_from_remote(&key).await?;
cache.lock().await.insert(key, value.clone());
Ok(value)
}
The clone calls are visible and explicit. The code is not as cache-friendly as a version with careful lifetime management might be. For an application that spends most of its time waiting on network I/O, that distinction does not show up in profiling.
The Performance Reality
High-level Rust sits closer to Go on the performance spectrum than to idiomatic low-level Rust. You get no GC pauses and no surprise allocations from a runtime, but you do get heap allocations from clones, atomic operations from Arc, and lock contention from Mutex.
For reference, Go’s throughput numbers for typical web service handlers tend to run in the 50,000 to 150,000 requests per second range on a single core. Idiomatic, allocation-minimizing Rust on the same workloads often reaches 200,000 to 500,000. High-level Rust with liberal cloning and Arc sharing typically lands somewhere between those poles, often closer to the Rust end, because even clone-heavy Rust avoids the GC overhead that eventually caps Go’s throughput under sustained load.
The Benchmarks Game numbers are a blunt instrument, but they consistently show that even performance-careless Rust outruns Go for CPU-bound workloads. For I/O-bound applications, the gap between high-level Rust and idiomatic Rust is often academic.
The more meaningful comparison is against Python, Ruby, or Node.js, which many high-level Rust adopters are replacing. The performance delta there is not marginal. A web service written in clone-heavy Rust with Tokio and Axum will routinely outperform its Python equivalent by an order of magnitude without any careful optimization.
Where This Breaks Down
High-level Rust starts to feel wrong in a few specific scenarios.
If you’re writing a parser, a codec, or anything that processes large buffers where avoiding copies is part of the correctness contract, the owned-types approach will fight you. Rust’s lifetime system exists largely to make zero-copy processing ergonomic, and deliberately avoiding it means manually boxing data or accepting copies that the problem domain cannot afford. Crates like zerocopy and bytes exist precisely because the high-level patterns are insufficient for this class of problem.
Embedded and systems programming is the other obvious case. No-std Rust without an allocator is a different language from the high-level variant described here. The patterns above require std and a heap.
Library code sits in between. If you’re writing a library intended for use by others, leaning on owned types and clones in your public API imposes costs on your users that they may not be able to absorb. The ergonomic convenience of String everywhere is fine for an application binary; it is a thoughtless choice in a widely-used crate. For internal services and application code, this is rarely a concern.
The Incremental Path
The argument for high-level Rust is ultimately an argument about entry points. Go was designed from the start to be learnable in a weekend and productive within a week. Rust was not designed that way, and the usual learning materials reflect that. The high-level approach described in the source article offers a different entry point: start with owned types, clone when in doubt, use anyhow for errors, and defer the ownership deep-dive until you encounter a problem that actually requires it.
What makes this viable in Rust that it is not in, say, C++ is precisely the guarantee structure. In C++, writing new everywhere and leaning on smart pointers gets you closer to correct, but the compiler cannot catch all the ways you can still get it wrong. In Rust, even the high-level patterns are backed by the same compile-time enforcement. The borrow checker is not optional. You’re working within it whether you’re using lifetimes aggressively or cloning your way around them.
The gradient from high-level Rust to idiomatic Rust is a path you can walk over time, encountering specific problems, reading Rust for Rustaceans when a chapter becomes relevant, and refactoring hot paths once the profiler tells you to. That incremental path is harder to follow in Go, where the language simply does not expose the tools for it once you need more control. In Rust, the tools are already there; high-level Rust just means you haven’t needed them yet.