I spent the weekend learning Rust properly rather than just reading about it, which I've been threatening to do since the 1.0 release went out last year. The plan was small and sensible: take a tiny log-parsing tool I'd written in Go, and port it. Same behaviour, different language, see what the experience feels like once you get past the syntax. What I actually got was a long, undignified argument with the compiler that I lost repeatedly, and then, slowly, started to win by changing my mind rather than my code.
The first wall
The first error came within about ten minutes, and it was the classic one everybody hits:
let mut entries = Vec::new();
for line in lines {
let parsed = parse(line);
entries.push(parsed);
process(&entries);
}
I'd written something morally equivalent to this and the compiler told me, politely but firmly, that I could not borrow entries while I was also mutating it, or hold a reference to something inside it while pushing more on. In Go I would never have thought twice. You take a slice, you append to it, you pass it around, and if you append past the capacity it gets reallocated underneath you and the old references quietly point at nothing. The difference is that Go lets you do this and Rust does not, and the thing Rust is stopping is a genuine bug, the kind that in C would be a use-after-free and in Go would be a subtle aliasing surprise.
My instinct, and I suspect everyone's instinct, was to treat this as the compiler being obstructive. There must be a way to tell it that I know what I'm doing. So I went looking for the override.
The escape hatches, and why I put them down
There are escape hatches. You can clone your way out of almost anything, and for the first hour that's exactly what I did. parsed.clone() here, entries.clone() there, and the errors went away. The program compiled. It also did roughly twice the allocation it needed to, and the code read like an apology. Cloning to silence the borrow checker is the Rust equivalent of catching an exception and doing nothing: it makes the message go away without addressing the thing the message was about.
The other temptation is Rc<RefCell<T>>, the shared-mutable-state pattern that lets you opt back into runtime borrow checking instead of compile-time. It's a perfectly legitimate tool and there are problems that genuinely need it. But reaching for it on day one, for a linear log parser, is using a sledgehammer because you can't be bothered to understand why the nut is stuck. I tried it, felt slightly dirty, and RefCell rewarded me with a panic at runtime when I double-borrowed anyway, which is exactly the sort of thing the compile-time checker would have caught for free. I'd taken a static error and bought myself a dynamic one. Poor trade.
Losing gracefully
The turning point was when I stopped asking "how do I make the compiler allow this" and started asking "why does the compiler think this is dangerous". Because it wasn't wrong. My loop was building up a vector and repeatedly handing out references into it and mutating it, all interleaved. That's three jobs tangled together, and the tangle is the actual problem. Go let me ship the tangle; Rust just refused to.
Once I separated the phases, the borrow checker went quiet, because there was nothing left to complain about:
// build it fully first
let entries: Vec<Entry> = lines.iter().map(|l| parse(l)).collect();
// then read it, immutably, all you like
process(&entries);
No clones. No Rc. No RefCell. The ownership is now obvious because the data flow is now obvious: gather, then process. Mutation happens in one place, reading happens in another, and the two phases don't overlap. The compiler had been pointing at exactly this the whole time, I just kept reading its messages as obstacles rather than as design review.
That reframing turned out to be the whole lesson of the weekend. Most of my fights with the borrow checker weren't really about lifetimes or references at all. They were about me trying to do several things at once in a place where Rust wants them teased apart. Iterators and collect solved more of my problems than any amount of clever lifetime annotation, because they nudged me toward gather-then-transform pipelines that have unambiguous ownership built in.
What I'm taking away
I'm not going to pretend it was painless. There's a real cost to the learning curve, and the first day genuinely is frustrating in a way that Go's first day simply isn't. Go's deal is that it's productive in an afternoon and you find the sharp edges at runtime, in production, at twenty past one in the morning. Rust's deal is that it's irritating on a Saturday and the sharp edges show up at compile time, at your desk, with a coffee. Having recently spent an evening chasing a Go memory leak that was entirely my own fault, I have a lot of time for the second deal.
The borrow checker is not your opponent. It's a slightly humourless senior engineer doing your code review before you've even run the thing, and it has caught real bugs in my toy program that I would have sworn weren't there. Losing the argument gracefully means accepting that when it and I disagree, the smart money is on the compiler. I'll keep the port going. The tool's not finished, but the parser runs, it's faster than the Go version, and I've stopped trying to win.