An often overlooked part of the Rust license is that within a year of usage, users are required to make a comparison to their (previously) favorite language. While I am a bit late, I am ready to pay my dues.

Given the overabundance of Rust vs X content, I'll try to cover only areas that haven't been discussed to death.

Lifetimes and Borrows

Did I say I wouldn't cover areas everyone has already talked about? I lied!

Rust's borrows and lifetimes are so good they are worth repeating. Like many others, I found lifetime issues a painful area when first learning Rust -- especially with async code.

However, after enough experience with them, the friction faded away and the benefits make it extremely hard to go back to writing Go code. After having the safety of passing only immutable references, moving values, etc, the Go strategy of "please don't mutate this" feels crazy. After having actually safe mutexes for so long, going back to Go I found myself scratching my head when I encountered a far-too-common Go bug of forgetting to protect a field with a mutex on a new codepath -- I had become so used to Rust preventing me from stupid things, I had pushed the menial tasks of verifying these types of bugs out of my mind and into the compiler.

While LLM coding claims to allow developers to push menial tasks to the LLM and focus on the bigger picture (with questionable success), Rust legitimately delivers it via its compiler.

Aside from some (common) async ergonomic issues, the one pain-point I have found myself running into is the viral nature of Arc. While the function coloring issue in async Rust is well established, a similar issue occurs with Arc. Consider a deep callpath:

top_level(Arc<A>)
-- middle_1(&A)
   -- middle_n(&A)
       -- bottom_level(Arc<A>)

Here I have an Arc, but only pass a reference of it to some functions. Because these functions don't need ownership, they just take a reference to avoid requiring an Arc. However, due to later refactoring, somewhere deeper in the callstack now actually could really use an Arc, making us have to rewrite large trees of calls to use Arc instead. Go doesn't have this problem at all, because there is no distinction between owned and borrowed (though, of course, there may be bugs at runtime if someone unexpectedly holds onto a copy unexpectedly!)

Perfectionism

One of the biggest differences between Go and Rust I have found is not in the language itself, but in the community.

The Rust community has a very clear bias towards perfectionism. For any task, no matter how tiny, there are probably 25 popular crates that optimize the task for different specific use cases. This is for things as simple as a string or as complex as TLS.

I have never used a custom string library, a custom hashmap library, a custom TLS library, or a custom HTTP library in Go. There are a few cases of these, but nothing near Rust. For most Go users, the defaults are good enough.

I attribute this not only to Go having a broader standard library, but also the culture and flexibility of the language. Because Go is already never going to be the cutting-edge of performance, making a string 5% faster is generally not a wise investment. Even if it was, Go doesn't provide as much control around low level details making it impossible to provide all the different variations Rust libraries provider.

In many cases, this is very nice. There is no need to make a decision on what library to use, and libraries are much more likely to interoperate together nicely. On the flip-side, though, when the bespoke Rust library that perfectly matches your usage pattern makes a hot-path in your application, the interoperability pains tend to be manageable.

Testing

At no point using Go did I ever think "Wow, I really love the Go testing library"; its pretty bare-bones. However, every moment I interact with testing in Rust I cannot think of anything else! I absolutely hate Rust testing.

  • Table driven tests, which I love, are impossible to do correctly in Rust. Macro-driven are a terrible approximation.
  • Rust cannot skip tests.
  • Rust cannot (reasonably) share setup between tests for the same reason it doesn't support table driven tests.
  • Rust has no native benchmarking or fuzzing.
  • Rust has a pattern of putting tests in the same file as the source code. Its possible to not do this, but it breaks tooling. The amount of times I have been terrified from unsafe code in tests that look like they are in the production code has shaved a few years off my life.

Overall Rust testing has driven me to not write tests (in some cases) because of these pains.

Tooling

When folks come from C++ (or similar) to Rust, they tend to love cargo. But coming from Go, I feel go is pretty consistently better in every regard.

  • go tool pprof is the GOAT. The Rust equivalent are far less integrated and less feature rich.
  • I much prefer dependency management with go mod than cargo. Dependency management is a complex topic so I won't claim Go is better than Rust, but I do find myself frustrated with cargo much more often than go.
  • Go static binaries are amazing for distribution and deployment. Rust options include glibc (obnoxious) or musl (atrocious performance).

Compile times

While many see the compile time story as simply "Go is fast, Rust is slow", I defer a bit in that I think Go is slow too!. Rust is too, but in my experience its not too bad; for a dev build I see about similar, if slightly slower, Rust builds. Of course, its impossible to have a true apples-to-apples comparison.

The one major difference, however, is that Go IDEs are able to incrementally update much faster (essentially instantly) than Rust. Waiting 3s for a build is not terrible, but waiting for 3s in an IDE is a major productivity hit.

Overview

Overall I really like Rust. I also like Go! Currently I am using Go in control planes interacting with Kubernetes, which is an area it does pretty well in, especially given the large ecosystem in that space and the lack of extreme performance sensitivity. For Rust, I am building more data plane proxy codebases, which is an area Rust does very well in! The level of control, safety, and performance there is critical to success. Both languages have a place depending on the use cases in my opinion.