Rust vs. Go in 2023
bitfieldconsulting.com
bitfieldconsulting.com
Having used both, Go hides its complexity behind the veneer of simplicity while being more tedious and error prone as the codebase grows while Rust brings the complexity to you upfront while preventing errors later. Personally, even simply due to language features like algebraic data types in other languages, I really have no reason to use Go anymore. There's an oft-said quip that says that the Go language designers took the last 50 years of programming language theory and simply...threw it away.
Plus, to those saying it's easier to hack stuff together in other languages than Rust, I simply clone everywhere and Rust then becomes the same as a garbage collected language. You don't have to deal with lifetimes and excessive cloning from the get go, deal with that after experimenting then optimize, same as you would for any other language. The plethora of language features in Rust make it easier to hack around in, in actuality.
[0] https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...
[1] https://fasterthanli.me/articles/lies-we-tell-ourselves-to-k...
I find Rust to be unintuitive, and I don't think it is a language designed to be a "problem solver.
So, what do I mean by a problem solver? If you have a problem and you write code by exploring different solutions, continuously improve, and write hacky stuff where the goal is to simply solve a problem.
Rust is not a problem solver; it is going to fight you all the way through because of its emphasis on programmatic correctness. With Rust, you have to have the solution first, consider the language-specific caveats, and write something in such a way that you don't go back and have to improve it. I suppose that is how programming is supposed to be done - in a clear and structured manner. But let's admit it, nobody does that.
On the other hand, Go is a language that sits between Python and Rust. Python is THE problem solver language, and Rust is an elegant and good etiquette language. Go just works as a hackable language with the benefits of a compiled language and type support.
My plan with Rust vs Go for the time being:
- If I need to solve a problem and can do continuous improvement on the codebase → Go - If I have the problem solved, I have the resources to do a rewrite → Rust
Perhaps it's more a question of background? Rust contains many features I already know from the ML family + it formalizes the way I've already been thinking about memory since the early 90's.
If you ever have to change lifetimes of existing functions/structs on a non-trivial Rust code base, you're screwed.
I forgot that I had also written a small cycle-by-cycle emulator of a simple CPU (8-bit data bus, 16-bit addr bus, 8 16-bit GPRs + a 16-bit PC + a single condition flag + an interrupt flag, timer interrupts, conditional jumps, call, ret, iret, mov, add, terminal I/O). That was also surprisingly easy.
I would like to use generators or something similar for a more realistic cycle-by-cycle emulator but I haven't gotten around to playing with that yet.
Generators and various kinds of coroutines are, yet again, just formalizations of the way I have been thinking for years.
Maybe other domains would be more challenging but I seriously doubt it. I do know that it will take me a couple more months before I'm really proficient in Rust -- including multi-threading -- and no longer fear macros.
You just described my reasons for switching from Rust to Go better than I ever have.
But having made a microservice in rust it is the most stable software I ever written, you are basically paying upfront any null reference and thread safety issue I would have needed to debug. It probably did not save me time, but it definitely was a better experience to write
But when I do work Go excels in (web architecture, especially), I'm not particularly keen on using Rust or Zig in it's place. While it's perfectly possible to build a Postgres-fronting, redis-cached microservice to manage users, auth, etc in Rust, it's going to be an exercise in frustration compared to something more streamlined for that task.
It's fine to explore with- and write proofs of concepts in- Rust.
In many ways, it's better suited than Python, Ruby, JS or PHP for this, for several reasons. I have too little production experience with go to know how it does here.
The Rust type checker, and easy, built-in test framework, make refactoring easy and certain. Much more than the common "fingers crossed let's deploy" refactoring I've experienced with Ruby and Python. And way more than the Yolo methodology commonly employed in JS and PHP. And fearless refactoring is a requirement for me to experiment and explore. Because once I know what and how I want it, refactoring is needed.
A good, fast testing framework helps me explore. Efficient and iterative. Rust (and Go) have much to offer here, whereas in Python, Ruby and even more so in PHP or JS, testing is an afterthought, third party, bring your own setup. In rust I can start writing a test immediately after 'cargo init experiment'. But I understand not for everyone exploration==tests.
The rediculous ease of deployment of both Rust and Go compared to Python, JS, PHP and worst, Ruby, is crucial when I'm figuring out if people need what I'm building. 'cargo build --release && scp target/build/foo example.com:/use/bin' and I'm hosted and deployed. It's less work than getting a Heroku connected to GitHub. For me, releasing fast, early and often is a crucial feature when exploring. Rust (and Go) have solved this. In fact, this is my main reason to build in Rust: I'm just fed up managing complex and ever breaking runtimes, (pip-, rb-, nv) envs, complex and ever breaking CI.
With rust, my initial versions just have ARCs, Box-es, unwrap()s and clone()s all over the place. It's how I circumvent the frustration, yet know the app is 'correct'. All those clones, unwraps, boxes, Arcs etc act as TODO's, bit without the "this will probably cause corrupted data, fix me!", that my TODOs in Ruby or Python came with when exploring.
If I’m doing something for fun I always go to Python though. Just makes everything seem incredibly natural, and given all the fancy interpreters it can be pretty fast.
In real world use ridiculously so.
The lack of exceptions, Option type or ability to chain or manipulate errors means every line ends up wrapped in a 90s style "if error" check. Which ends up making code less robust than languages with more modern approaches like Rust, Scala, Swift etc.
The method type signatures clearly specify what exceptions will be thrown.
And the compiler requires you to deliberately handle those exceptions.
But I wouldn't consider Java to be the best approach for error handling anyway.
What makes it '90s style' other than you don't like it?
Calling something "90s style" and its alternative "modern" is a thought-terminating cliche.
- it returns both a return value and the error value, not xor. The convention is to use the error value only if returned, but sometimes the other “slot” is also used. If you squint a bit, this is exactly what errno does in C. The better, ML-y or exception-y way gives you a sum type instead, which can’t be mishandled.
- you have to handle it in-place with no convenient shorthand allowing for handling it at another layer (and usually that’s the only meaningful way forward). Also, error handling is a cross-cutting concern, squeezing it inside your business logic is (in my opinion) a bad thing.
Whether or not it's better to have a sum type (IMO it isn't), what Go has isn't anything like errno. errno is an ugly wart on the POSIX interface, which is otherwise quite nice. For example, Linux system calls directly return negative error codes, which is a much better way of handling errors, and then the libc has to set errno and return -1. Awful.
>- you have to handle it in-place with no convenient shorthand allowing for handling it at another layer (and usually that’s the only meaningful way forward). Also, error handling is a cross-cutting concern, squeezing it inside your business logic is (in my opinion) a bad thing.
That you say this and then advocate for optional<T>/result<t,e> is insane. That's exactly what a sum type does: it squeezes error handling into your business logic. And no, papering over that with ugly ? operators everywhere is not a solution, it is syntactic sugar. Error handling is part of the logic of your program. It is not just some 'ah we'll return it' thing. Many operations can fail and many errors are recoverable.
Which is an implementation detail, has nothing to with its semantics what we discuss here. Certain C compilers do wild things with errno as well.
I’m not advocating for optional/result types, I personally believe that checked exceptions are the ideal solution, but it is a thoroughly underutilized tool in PL designs (Java has it, though it has many warts, but that is not inherent to the concept). Hopefully with the wake of effect-typed languages it could change.
With that said, in Rust’s low-level case it makes sense to avoid exceptions, and sum types are an okay solution, especially if it has a handful macro for bubbling up. What go has is just straight up wrong.
Monadic error handling (Rust's Option<T>/Result<T, E>) avoids that issue by putting the error inside the return type as a normal value.
(Which is why, on multiple occasions, I've seen people call Result<T, E> "checked exceptions done right/properly".)
But they fail at including a stacktrace, autobubbling up, auto-unwrap and customizable "hit radius" with a try-catch block (okay, I'm sure some Monadic construct allows for that, but e.g. Rust's version does not).
(Plus, true exception handling is messy for a language that cares so much about FFI. Look at the discussions surrounding how panic! unwinding should behave at FFI boundaries.)
2. I think completely automatic bubbling would be a misfeature, and the `?` operator is a good balance between concise ease-of-use and the Rust focus on "code is read far more often than it's written".
3. Again, the `?` operator. I think completely automatic unwrap would also be a massive footgun and comprehension hazard.
4. As mentioned in the other reply, try blocks are in development. (See my previous comment about Rust adopting a Minimum Viable Product approach to v1.0)
https://caniuse.rs/ is a great illustration of how far we've already come and how minimal Rust 1.0 was.
Some examples (not an exhaustive list):
1. Fibers/stackful coroutines (See "Fibers under the magnifying glass" by Gor Nishanov for why they were hugely popular in the 90s and then got abandoned by all the major OSes and programming languages → http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p136...)
2. Garbage collection (The 90s were the height of GC hype, to the point that the JVM still struggles with "Don't worry. GC research will make it fast." design decisions.)
3. Error handling that feels like "Well, we know that exceptions were a mistake, so the only other option is C-style return values... but, hey, we can at least follow 90s Python's lead in supporting returning tuples." (Granted, exceptions DO bake in a fundamental assumption that all the world can be treated as single-threaded execution amenable to transactional rollback. → https://www.lighterra.com/papers/exceptionsharmful/)
4. No generics until they were pushed into it by things like people writing preprocessors that use the Canadian Aboriginal Syllabics characters that look like angle brackets. (https://www.reddit.com/r/rust/comments/5penft/parallelizing_...)
5. No sum types (Bear in mind that sum types have been around since ALGOL 68 and, on many fronts, C was a step backward that the entire industry copied)
6. Dependency management that was only a hair above "just `git clone` it into your project" for most of Go's lifetime
An example of such a reference would be this quote by rfiat at https://news.ycombinator.com/item?id=31603329
> It's interesting that you find Go to be Rust-adjacent. Setting aside the USPs of each language (goroutines and borrow checker respectively) and just speaking about the general experience of writing code, I find Go painful for all the reasons that I find Rust pleasant.
> The best summary I can give of Rust is that it was designed by people who learned from the mistakes of the programming languages that came before it. The best summary I can give of Go is to quote Rob Pike's response [0] to a request for syntax highlighting in the Go playground:
>> Syntax highlighting is juvenile. When I was a child, I was taught arithmetic using colored rods (http://en.wikipedia.org/wiki/Cuisenaire_rods). I grew up and today I use monochromatic numerals.
> [0] https://groups.google.com/g/golang-nuts/c/hJHCAaiL0so/m/kG3B...
dgunay's reply in that same thread ended on the summarization "I like Go and I feel very productive in it, but its commitment to simplicity is dogmatic in many ways and it very much is missing milestone advancements in PLs from the past several decades. It could easily have been made in the 90s."
This. It's Rust, not Go, that has replaced Python for me in any use-case more than a few lines long where the ecosystem is ready (i.e. when I don't need a GUI (PyQt/PySide), SQL (SQLAlchemy+Alembic and Django ORM autogenerate draft migrations by diffing the schema against the DB), or complex web stuff (Django's giant ecosystem of reusable components)), because...
1. Boilerplate like `if err == nil` is demotivating
2. "If it compiles, it works" is very motivating and, aside from having `#[test]`, a type system constraint is worth a thousand unit tests.
Heck, as efforts continue toward things like making LLD the default linker (40-50% reduction in link times) and making an official/stock interpretation of the rust-script utility (https://rust-script.org/), and as I work toward replacing my dual-core AMD CPU from 2011, Rust is going to push even further into being what I use to write "shell scripts" because "It works. Don't ** with it" is the number one psychological effect I have to push back against in other languages.
IMO those who say Rust isn't suitable for hacking seem to try to conform to best practices right away when really, dealing with lifetimes and excessive clones should be part of the optimization step, not the ideation and experimentation steps. Rust is as high level as you want it to be. Hell, I write web backends and sometimes even frontends in just Rust and I never worry about memory until I want to optimize, if I even want to optimize.
This part of the comment deserves to be highlighted and mentioned more. I think this exactly the way to "hack something together quickly" in rust.
After that, incorporating correctness seems to be as simple as grepping with each unwrap and "dealing with it".
With the above approach, Rust is a great prototyping language as well.
With unwrap, replacing the unwrap with an expect signals you've accepted that an unwrap is ok in that piece of code. I'd like a similar convention for clone.
Rust code lets be confident that when I make changes as I explore the problem/solution space that my program will actually work.
I write Rust professionally and what you hate about Rust is opposite to why I love it so much. Especially in larger projects, I have never in all my career had an easier time hacking and iterating. I'm infinitely more confident in writing a ton of lines that will work once I compile. In Python and Go, I could end up chasing silly bugs and waste time debugging and tracing to find that I made a typo or ran into a language quirk that gave me an unexpected nil pointer. That situation is almost non-existent in Rust, it's just me and the problem. Rust is honest and upfront about its quirks and will yell at you about it before you have a hard to find bug in production.
Those last two sentences aren't 100% true. Advanced Rust can be tough and you absolutely will fight with it. But to be perfectly honest, this gets rare over time and usually it comes from a lack of understanding. When you finally do understand, it's another tool to add to your belt that makes you a better programmer.
Only thing I'll add is. 8 times out of 10 if you are reaching for advanced rust, you are over engineering. Most software projects don't require it and when they do, you should always question whether there's a simpler more maintainable way or make a other design decision. There is no award for least lines of code, or most utilized compiler. Software is social.
That's Go's biggest strength over Rust and Scala. Good luck getting job security in that language. It takes a few years to really know what I'm saying but in my opinion it's the difference between a senior engineer and a non senior.
And that's absolutely not how you do it in Rust either. Once you've internalized the Rules of the language, you can definitely grok your solution in Rust the same way you'd do in Python, the biggest difference being that Rust will help you a lot more when refactoring your code to the next iteration, so you'd be more efficient over a few days of work in Rust than in Python.
The big caveat being that you need to actually learn the language first instead of pretending you're a polyglot developer and you can write code in any language without having to spend time learning it, but maybe you'd argue that nobody does that either…
I prefer go for cloud integrations because they tend to have the best implementations and most solid APIs. That's about it though.
Python is a good bash replacement, but I don't like writing large projects in it. It's hard to maintain and the errors that come from production are never straightforward. Good for some medium scale mathy compute projects though.
C++ is where I write most math code after prototyping it elsewhere(R or Python)
Rust for me is the answer to most things except, it's ecosystem is a bit weak, and it's a bit of a clout chaser language(not as bad as others though)
That's sort of true, but I wish people would stop cowardly saying that you can't say that one language is better than another overall. That's clearly not true.
IMO this article is very very long and doesn't really highlight the differences well, which IMO are:
* Go has much faster compile times.
* Rust's tooling is very good but I would still say Go's is better. It has built in fuzzing support! Cross compilation is as simple as setting an environment variable.
* Rust is a little faster, and avoids GC.
* Go's standard library is much more comprehensive than Rust's.
* Go is much easier to learn and to read.
* Rust is a better language if you have learnt it. It has much more powerful features (especially the functional ones) and is more principled. Going back to Go from Rust is frustrating.
* Rust has a much stronger type system and the borrow checker. Both make bugs far less likely.
* Async Rust is probably the worst bit about Rust. Go's goroutines are much easier to use. Though I would say traditional multithreaded code in Rust is pretty easy - it's just that a lot of the ecosystem forces you into async.
I also found this sentence to be confusing:
> If you only have time to invest in learning one language well, don’t make your final decision until you’ve used both Go and Rust for a variety of different kinds of programs, large and small.
So, if I only have the time to learn one language, I should...use two languages? For multiple projects? But somehow without learning them, because I don't have time?
Even if you mostly write Rust (or whatever), it’s still very useful to know other languages. Learning C will teach you how rust works under the hood, and teach you a lot about FFI systems everywhere. Learning javascript will be a massive help if you do web programming. I’m learning ML at the moment and python for that seems like a hard requirement. If you develop for Apple’s platforms, Swift is important to know. And so on.
And most languages just aren’t that hard to learn anyway. If you’re an experienced programmer, learning enough Go to be dangerous is a weekend project. It’s just not that much work.
The notion of "last language" that I used is, however, a bit different. It just meant that if you've implemented something in Rust, you won't need to rewrite in C or C++ due to presence of garbage collection or a need to integrate it as a library.
How so? Rust don't use C under the hood. (And even in ffi, you should just know the limitations of C to know what you can't do in FFI)
Terrible idea.
Everywhere else people ask for who's responsible when products don't match expectations, or are sold with faulty capabilities, unless the consumer protection laws of the country suck in protecting the citizens.
distributions are going to love your module! /s
There was a huge kerfuffle about it.
https://blog.yossarian.net/2021/02/28/Weird-architectures-we...
Depending on what you're doing.
I am finding that Rust is easily beating all of the non-C languages in terms of full support for the latest performance technologies e.g. io_uring, SIMD (AVX2/AVX512/NEON), DirectStorage, CUDA etc as well as the highest quality wrappers for C/C++ libraries.
So for many applications Rust will simply leave Go for dust.
In fact for me the only benefits of Go are single binary and startup time. Otherwise I find Rust to be ideal for when I need low level performance. And Scala to be significantly better for backend apps since its faster, has access to more libraries, better concurrency and a stronger type system resulting in much more reliable code.
I hope you mean "compilation time" or "time to first prototype". My experience has been that, if a Rust binary doesn't start instantly, it's the fault of a specific library, not the language in general.
Also, they ARE working on single-binary deployment for Rust... they just have a much more "Take your time and do it right. Don't hack around ecosystem flaws" approach than Go.
If you're not cross-compiling/cross-deploying, Rust is already on par with non-cgo Go... you just need to use a musl-libc build target for Linux, specify RUSTFLAGS="-C target-feature=+crt-static" for Windows, etc. to get statically linked libc.
(The musl-libc targets are a simple `rustup target add` away if your only C dependency is the libc itself.)
I always wondered: how is it not much faster? Dealing with the borrow checker is immense overhead, yet performance gains are low compared to Go. Is Go's GC simply that good? Is Rust's compile-time GC about something other than performance somehow?
I realize Go still needs to ship a minimal runtime in its binaries, but don't feel that's a meaningful burden compared to borrow checker fighting.
Predictability and determinism: In some environments, GC pauses are problematic.
Modern GCs in Go and Java running on fast enough CPUs can generally keep pause times down below 1 ms, but on a multi-GHz CPU that's multiple million clock cycles - a virtual eternity in some contexts.
Memory usage: GCs waste memory in exchange for performance. Go's GC doesn't compact (last I checked), so fragments the heap.
Ability to have no runtime: in some environments, the need for a runtime is problematic. That's a big part of why the Linux kernel supports Rust but not Go.
All of these issues tend to be more significant in e.g. hard real time contexts, in embedded environments with less powerful processors, etc.
Also btw "borrow checker fighting" is not much of an issue for experienced Rust developers. You quickly get used to how to write code to avoid borrow checker issues. And in general, writing code that way tends to be a good thing anyway.
Both of your sentences are true, but are not related. GCs indeed make a memory tradeoff for better performance, even if it is a compacting GC. But dynamic allocation even with Rust does fragment the memory and is in fact solved better by a compacting GC.
It's also stuck on an old fork of tcmalloc which isn't anywhere near the state of the art today and will only fall further behind if they don't at least start on a plan to make allocators interchangeable. Bear in mind that Rust has had interchangeable allocators for years, and Java has had interchangeable GCs for decades, while Go still has neither.
Go's GC is pretty good for what it is, especially now that it has become more of a control system that converges on a target collection time ratio. But it is neither a state of the art GC -- not even by the standards of 10 years ago given how far along the JVM was already -- nor is [de]allocation done with a state of the art allocator.
People praise that Go promises a 10ms max duration for GC Assist -- the mechanism that makes your actual critical-path work pause and help out the struggling GC -- and while 10ms is fine for a logs pipeline or something, it's extraordinarily bad for many kinds of API endpoints which should have a 99th percentile latency under 1ms (bearing in mind that even local networking is going to add to that considerably so the floor should start low).
That's despite Go's GC being hailed for targeting predictable low latency rather than overall throughput... this is the predictable low latency they're talking about. 10ms is a failure in a world of sub-ms targets, and Go as a language gives you very limited options to avoid it, even after abandonding community idioms and popular libraries.
By copying more?
Also, by knowing when and how to use wrappers like RC, Arc, RefCell, Box, etc., and by knowing when and how to use lifetime annotations.
As for copying, you learn when to move, when to copy, when to clone, and when to implement the relevant traits on your types.
It's really not that different from working in a language like C or C++, except that the compiler ensures that you're not making any mistakes.
With that said, I find it questionable to say that Go would be faster than Rust - the former does barely any optimizations (hence its fast compile time).
AFAIK, memory safety and language features as RAII is also available in C++, for instance. About the reasons for slow compilation, take a look at https://www.reddit.com/r/rust/comments/xna9mb/why_are_rust_p...
Not having a GC is also about not having a runtime as you mention (e.g. nice for creating Python extensions and embedded systems programming) and also more runtime deterministic performance: on that, if I'm not mistaken that was the reason for Discourse switching to Rust and also, e.g.: "the choice of Rust as the main execution language avoids the overhead of GC pauses and results in deterministic processing times" https://github.com/apache/arrow-ballista/blob/main/README.md
With idiomatic non GC programs, an object that is freed is deallocated at that point of execution.
Think of a loop running 15 times that calls a function which allocates objects, uses them and then returns.
In a GC language, there is only a single free. In a non GC language, each return of that function will cause a free to occur.
There's some other interesting optimisations that can only be done in a GC language.
The only way to get those optimisations in a non-gc language is to effectively implement your own allocation and deallocation using pools.
Let me just focus on one very commonly used data structure: hash maps. Rust hash maps:
* Use SwissTable because they didn't make a short-sighted promise about delete-while-iterating behavior that makes Go incompatible with SwissTable's tombstone design and makes Go never shrink maps once created.
* Let you customize the hash function to whatever performs best for your key type & target hardware.
* Let you find & insert/update in one hash table lookup, where Go always requires at least two, and its compiler doesn't optimize even the most common idioms.
* Let you reference a key/value in-place instead of having to copy it out each time. Go won't allow this because the reference may become stale, but Rust can enforce this with lifetimes.
* Let you clone a map without rehashing every key to a new seed. I generally measure at least 15x speedup from this alone, unlocking very useful design patterns like "clone a map and apply a few temporary updates for a one-off operation like validation or simulation" with no extra code complexity. Go gives you no better option than slowly rehashing the entire map.
And that's just hash maps. How about Go's regex engine being one of the slowest in the world while Rust's regex crate being one of the fastest:
https://github.com/mariomka/regex-benchmark#optimized
https://github.com/BurntSushi/rebar#summary-of-search-time-b...
Further, Go refusing to have macros means that many libraries use reflection instead, which often makes those parts of the Go program perform no better than Python and in some cases worse. Rust can just generate all of that at compile time with macros, and optimize them with LLVM like any other code. Some Go libraries go to enormous lengths to reduce reflection overhead, but that's hard to justify for most things, and hard to maintain even once done. The legendary https://github.com/segmentio/encoding seems to be abandoned now and progress on Go JSON in general seems to have died with https://github.com/go-json-experiment/json . Remember that next time someone parrots the myth that Go is ideal for HTTP JSON APIs.
Many people claiming their projects are IO-bound are just assuming that's the case because most of the time is spent in their input reader. If they actually measured they'd see it's not even saturating a 100Mbps link, let alone 1-100Gbps, so by definition it is not IO-bound. Even if they didn't need more throughput than that, they still could have put those cycles to better use or at worst saved energy. Isn't that what people like to say about Go vs Python, that Go saves energy? Sure, but it still burns a lot more energy than it would if it had macros. Maybe someone's recipe website doesn't care but world-scale datacenter applications do.
Rust can use state-of-the-art memory allocators like mimalloc, while Go is still stuck on an old fork of tcmalloc, and not just tcmalloc in its original C, but transpiled to Go so it optimizes much less than LLVM would optimize it. (Many people benchmarking them forget to even try substitute allocators in Rust, so they're actually underestimating just how much faster Rust is)
Oh yeah, aside, tcmalloc was designed for C++ code with lots of temporary allocations that are allocated & freed by the same thread so they're very cheap when cached per thread/core. Go almost always frees things on a separate GC thread, completely undermining this design goal. So tcmalloc isn't even a good fit for Go, yet there's no other choice available. People don't even discuss it any more because what is there to discuss, Go and all of its users are just stuck with it.
Finally, even Go Generics have failed to improve performance, and in many cases can make it unimaginably worse through -- I kid you not -- global lock contention hidden behind innocent type assertion syntax: https://planetscale.com/blog/generics-can-make-your-go-code-...
It's not even close. There are many reasons Go is a lot slower than Rust and many of them are likely to remain forever. Most of them have not seen meaningful progress in a decade or more, and I'm not holding my breath for the next decade either.
True. Golang multithreading is easy to reason to about. I had to make some mental gymnastics to write multithreaded scripts in Ruby and Python. The Golang implementation was the easiest by far.
I really prefer strong type systems over weak ones, but despite that, I always like to point out that there's no evidence that strong type systems cause "bugs to be far less likely". No study I've seen (quite a few) has ever shown that.
There's a few bugs that become impossible (in Rust, some kinds of data races for example) but the vast majority of bugs can still occur and in practice, do occur.
It's quite similar to how using immutable data structures leads to less bug prone code.
There's a really great one that shows that Typescript or Flow prevents around 15% of Javascript bugs.
It would be difficult for a study to show that Rust's strong typing or borrow checker prevents bugs, but anyone who has used Rust extensively knows it to be true. It's similar to Haskell's strong types and immutability preventing bugs. Google "if it compiles it works".
I think you could do a study to prove this, but experiments aren't the only way to get knowledge. They are the best when possible but sometimes they are impractical. Programming language studies tend to focus on "get some students to spend a few hours writing code from scratch" tasks which can even lead to conclusions that are the opposite of reality for realistic programming. For example that static types hurt productivity.
> the vast majority of bugs can still occur and in practice, do occur.
Right, but they are much less likely to occur.
> anyone who has used Rust extensively knows it to be true.
I have used Rust extensively and have no idea whatsoever is that's true, and if you think you do, be prepared to be wrong about that, as humans are incredibly "good" at being sure that something is true until formal studies show you were wrong.
Haskellers have said "if it compiles it works" for a long time, but have never been able to prove that. Again: just saying/feeling something is true doesn't make it true.
> Right, but they are much less likely to occur.
No, there's just no evidence! Just go to any Rust project, they have as many bugs as anything else... I wish that were false, because then I would definitely just use Rust for everything myself... but that's not at all what I've seen so far.
Also, bear in mind that it's not just Rust's type system directly that prevents bugs, but also the patterns it enables. For example, the typestate pattern and its use in things like turning misuse of the HTTP protocol into a compile-time error in Hyper.
- Go (the language) is missing the ability to declare something immutable. I don't mean making fields in a struct private then exposing getters. I mean the equivalent of an immutable reference in Rust or `const ref` in C++.
- Go mod and go workspace still can't get a cohesive story together [1]: - - If my module wants to depend on another module in a workspace, `go mod tidy` would freak out. - - If I use replace in my go mod file to point to the other modules in the same workspace, `go mod vendor` will copy these workspace modules to the vendor folder as well.
* Feels closer to the good old Pascal days where you control everything
* Safer bet should wasm & friends explode
* Already have python for quick & dirt so picking something more extreme made sense. Go feels sorta in the middle
* Its in kernel & loads of hype so unlikely to go away
With that said, Go indeed gives you plenty of rope to hang yourself with in relation to concurrency (among others).
Async Rust, OTOH, can quickly become a horrible spaghetti mess that's virtually impossible to debug, and where common things such as cancellation are very challenging.
My networks servers written in Rust are unsafe.
I know they have dead locks, memory safety issues (due to usage or shared arrays and slabs to avoid allocations and work around the borrow checker), can be remotely DOS'd, leak memory (due to Rc, cycles or whatever), and have hacks to work around APIs that are not easy to use, just to make the borrow checker happy. Doing simple things such as listening to multiple IP addresses can require a major refactor.
At the end of the day, after the excitement of having written a first version of these projects, I have a hard time understanding and fixing the remaining bugs, and eventually stop maintaining them.
The ecosystem is also unstable. Dealing with major changes, or crates that need to be replaced because of deprecation/abandon makes applications unsafe. "crap, after cargo upgrade, that doesn't compile any more. Let's randomly fiddle around so that it compiles again". The version with the new crates doesn't get tested as much as the previous one. If they behave differently, or if to get the behavior of function X, now one should call function Y because X now does something else, the application is not as safe as when originally written.
My network servers written in Go probably have plenty of bugs, but I can understand what's going on and fix them. I can dive into the code of the dependencies and of the standard library, follow and understand how things work under the hood. With Go's rich standard library, I also don't need to rely on a ton of dependencies, that will constantly be abandoned or introduce breaking API changes.
So I can stay focused on the application logic, and eventually, the Go servers are way safer from a practical perspective. And for something that must be maintained on the long term, Go also wins hands down.
> Which is better, Rust or Go? Go or Rust? Which language should you choose for your next project in 2023, and why?
I blame the "rewrite everything in Rust" crowd, but there seems to be a resurgence lately of the idea that there is One True Programming Language you should use for everything.
I don't particularly fancy using an OS written in Go, and I don't want to work on web architecture projects in Rust. Similarly, I don't want to work with someone who is going to write shell scripts or basic utility tasks in Rust because the performance is faster.
I'd much rather pay a little extra up-front in resolving compiler errors, so I can know I'm not going to wake up to something like a six-hour batch job that failed 5% through or slam face-first into needing to debug an exception when I'm already in a rush and stressed over it.
Likewise, I find Rust's syntax much more pleasant than Go's... probably because I can see that Rust's syntactic complexity means things that have value to me, while Go's is just repetitive boilerplate to make my time a sacrifice on the altar of dogmatic language simplicity.
- Go (the language) is missing the ability to declare something immutable. I don't mean making fields in a struct private then exposing getters. I mean the equivalent of an immutable reference in Rust or `const ref` in C++.
- Go mod and go workspace still can't get a cohesive story together [1]: - - If my module wants to depend on another module in a workspace, `go mod tidy` would freak out. - - If I use replace in my go mod file to point to the other modules in the same workspace, `go mod vendor` will copy these workspace modules to the vendor folder as well.
In my experience, compared to Golang, the only (mild) downside of Rust is not so big STD lib, compilation time and initial learning curve. Frankly speaking its neither a blocker nor a big deal. Once learning phase of Rust is passed, you have one language which you can use anywhere and can trust to produce efficient code both from readability and performance point of view.
- Rust is not suitable for fast paste startup environment
- Rust is not suitable if the initial team members are not Rust experts
- Rust is not suitable if the startup doesn't have money (Rust devs are very expensive and scarce)
- Rust is not general hire friendly
- Rust is not ideal for iterative and high refactor potential of startup culture
- Rust is painful/heartbreaking when eventuality of a pivot approaches
- Rust is not an advantage in anything startup related
Go is the exact opposite of above points! Note: I didn't talk about performance, feature or whatever else ... both languages are very very close to each other!
Need some data science work done, easy. Need a web server done, easy. Need things to be fast and relatively performant done, easy.
> fast
This is only true if you are relying mainly on compiled extensions to do the performance-critical work. Unassisted raw python is super slow compared to Go or Rust.
If you want to write slow, dynamic, hacky code in Rust you have to write 'unwrap' and 'clone' all over the place, obscuring the logic.
Some people in this thread have made the claim that Rust is okay for quick hacky code to work out a solution to a problem, because you can just pepper your code with unwrap and clone and then factor them out later during optimisation. The point I am making is that even if that approach works[0], it heavily obscures your business logic to have it peppered with memory management details when you're just trying to figure out a basic algorithm to solve a problem.
The approach I much prefer is to Plan to Throw One Away[1]. I'd rather work out the solution to my problem in a slow and dynamic language like Python, and then either rewrite it entirely in C or, if possible, just rewrite some inner loop in C and call it from Python.
[0]: I don't think this approach actually works. You can't take a program that has been designed around copying data absolutely everywhere, all over the place, and make it very efficient. You can make it more efficient than it was originally, but I've never seen it done. You can only really work out the right way to manage memory when you know the algorithm, and you can only know the algorithm once you are done. Refactoring from one memory management strategy to another might be possible in theory but is awful in practice, even in Rust.
In rust there are two forms of memory management, 1. You write rust. 2. You write unsafe rust.
You never really have to refactor to suite a different style. Infact changing allocators is as simple as running "cargo install".
Rust isn't as bad as people make it out to be but you do have to invest some time to learning it.
But as far as throw one away goes... You can write it in rust, and then slightly adjust when you do things like clone so nothing gets thrown away unless you really botched it the first time you wrote it.
For example:
Geeqie is an image viewer/manager. It's a fork of the abandoned GQView and is apparently made in the image of Windows 9x-era ACDSee. It's written in C++ and uses GdkPixbuf.
My first prototype for re-creating Geeqie's collection view in PyQt so I could integrate it into a different kind of image management tool was twice as fast at loading thumbnails.
(Something on the order of 20s vs. 10s for loading somewhere around 7000 thumbnails on a 65W TDP dual-core Athlon from 2011.)
I tried quickly benchmarking GdkPixbuf, QImage, Pillow, and the Rust image crate against each other and found that, if anything, GdkPixbuf is a little faster than PyQt... but whatever Geeqie is doing cuts that performance in half.
(And that's with the Python version clamped to single-threaded image loading because I haven't yet written a smart I/O scheduler to allow parallelism on SSDs while avoiding thrashing rotating drives. Experiments with the QThreadPool-limiting line commented out show a speed-up from 10s to roughly 7s, consistent with how much of the overall system CPU capacity was taken up with other things like Firefox at the time.)
Jobs to be done and the fastest/cheapest way to get there!
I'd also argue that python and performance is totally untrue. Python is probably only second to bitcoin mining in terms of wasted CPU time and global CO2 emissions. Every python programmer that switches to go or rust is the equivalent of removing 15 cars off of the road after all.
Oh, and performance. Not even close. Go is easily 20x performant in each case we've migrated.
Also, Python is super slow comparatively and is only fast if it delegates to other languages.
Not pooping on python it's... Fine. But I am not of the opinion that it's a great choice for all projects because technically "it can" do "anything".
The Rust ecosystem has a broad collection of tooling for using Rust to extend other languages, so there's minimal chance something I wrote in Rust will have to be rewritten to use it in a project in another language.
See https://www.hobofan.com/rust-interop/ and https://areweextendingyet.github.io/
In Python's case, there's also maturin, which aims to make building and publishing Rust-based wheels as easy as Cargo makes doing so for Rust-based crates.
...plus, in the worst case, Rust CLI tools start very quickly, so forking a Rust process for poor man's FFI is more viable, and Serde plus a JSON Schema generator like schemars will streamline microservice-based options.
We tried both Rust and Go
-Rust is very suitable for fast pace environments because I don't have to worry at all about a whole class of issues
- Almost all of us learned Rust on the job and it worked quite fine. The more you learn the better you get, same is probably true for other languages
- We found that Rust devs are not more expensive, quite the opposite. We have people willing to switch jobs just so they can develop in Rust
- We found that adding "rust" to the job adverts increases engagement by .. a lot
- The others are even more subjective and also not true for us
I don't doubt that this was your experience at all. This might even be a majority opinion even though I doubt it. But it is definitely not true for everybody.
Rust is as high level and flexible as one wants, just clone everywhere when experimenting and clean it up when you arrive at the final solution.
The sentiment of "our startup became a success because of Rust" is misleading. You're success is not attributed to choosing Rust over something else but rather the effort spent in gaining valuable expertise in years prior and understanding the product it takes to gain customers' attention/money/commitment.
You equate Rust to "takes more time and money". Neither of those is true (for us) compared to Java and Go. Both of which are languages we initially tried.
But I do agree that the choice of programming language is largely irrelevant to the success of a company. Somehow you seem to think it does, or rather that Rust specifically is the one case where the above is not true.
I am part of a small startup, an SaaS product with 2 Million MAU. Our initial code was in Python and we absolutely struggled with it once we hit scale. Refactoring and adding features were really scary prospects.
The choice of picking Rust was taken after much soul searching and double guessing. We had even ported over one of the services to Golang before we switched to Rust. Most importantly, the choice to use Rust was not mine, it was that of another colleague of mine.
Rust turned out to be the best technical bet we ever took. Of course, since we were in fintech, there was a huge emphasis on correctness which was near impossible to get right in Python.
In Golang, we got the performance we wanted but it just had too many footguns that repeatedly came back to bite us. For context, we have a large number of very stateful and computationally intensive services running. There's a lot of concurrent mutations of shared data we had to deal with. Doing this in Golang was an absolute nightmare. Even after religiously using golangci-lint and fixing issues, we kept running into data race issues.
Rust has never given us these issues. I'd much much rather fight an overzealous compiler pointing out all my bugs at compile time rather than running into a nuanced race related Heisenbug that I need to debug in production, with millions of users' money on the line.
As for developer productivity, devs in our team are desperately pushing to port over _everything_ to Rust because of the peace of mind it gives them, pushing to production. People groan at the prospect of having to update some of the yet to be rewritten Golang code and riot when asked to deal with the legacy Python services.
There's one important caveat I'd like to add to anyone reading this and reaching for Rust. No one else in our team (including me) except one of my colleagues knew Rust when we set off to try Rust. However, we had a big advantage. That one person who had proposed the Rust refactoring had expert level of Rust knowledge.
Not all companies might have this luxury and his expertise may have been what ended up getting over all the initial humps that may have derailed us initially as everyone else was coming to grips with Rust.
Freudian slip ... copy pasta environment?
This one is true, the others very much not!
Let's group them in two categories: HR and pace.
> - Rust is not suitable if the startup doesn't have money (Rust devs are very expensive and scarce)
> - Rust is not general hire friendly
Rust developers aren't plenty, but neither are Rust jobs so it's not harder to find candidates than with any other languages. And since we're still in the early-adopters stage, the candidate pool is of much higher quality in average. And people are in fact happy to be able to land a job in a language that they like so much, so they aren't particularly expensive either (a bit like video game developers).
> Rust is not suitable for fast paste startup environment
> Rust is not ideal for iterative and high refactor potential of startup culture
This is very much the opposite! Rust is arguably S-tier when it comes to refactoring, the strong typing making it almost impossible to cause regressions during refactoring, including concurrent and parallel code, this is probably its biggest strength overall and its unparalleled in any language I'm familiar with (that is C#, Java, JS, typescript and Go).
Overall it's a bit awkward to see that you're use two strengths of Rust as if it was liabilities.
This had to be said, well done! It feels like you get bashed for criticizing Rust just a little nowadays.
* Go was developed within Google, mainly as a response to C++ projects that had grown unwieldy, in terms of dependencies and compilation speed. As a result, the language is designed from scratch with compilation speed in mind, and unused dependencies are disallowed.
* There are few changes to the language. When a new Go compiler is released, I go "nice, wonder which tooling and performance improvements are present" and not "oh no, how many repositories will need patches". Go code can be expected to compile 10+ years ahead. The same is not always the case for C, C++, Zig and possibly even Rust.
* Deployment and crosscompilation is straightforward.
That's just completely not true, especially for C. There is a lot of C code older than most commenters on this site that's still used in production and compiled with modern compilers
This is patently false. Rust has a severe backwards compatibility policy.
I'm sorry, but this comment just seems so blind to reality that it's hard not to comment on. C has a few particularly strong selling points:
* Orthogonality to machine architectures
* Freestanding (STD-free, in modern language parlance) compilation
* An extremely conservative ruling body
The latter is usually listed as a double-edged sword, since there are many new features people would like in C: true generics, polymorphic (overloaded) functions (without using the pre-processor), a true module system, etc; but it also means extremely old code compiles just fine on modern C compilers. Now, it might not work because it might be built for some bespoke hardware/environment, but the code will compile fine and be correct.
For instance, here's a perfectly valid ANSI C application that will compile on any C compiler in the last 35 years (three times as long as Go has existed):
https://godbolt.org/z/ja6WM5feT
Most of the features added in C11, C17 and C23 are additional features, usually with no backwards breaking changes. However, in some cases they do clarify undefined behavior; which can break old code. This is why most of the major modern compilers add flags to compile in specific dialects.
Additionally, there are plenty of Go 1.0 (the first [and only] "10+" [11] year old version) codebases that won't compile without major changes.
Source: Arch Linux package maintainer for 10+ years
And couldn't compile my 9 year old Go project (a small supervisord clone) a few month ago (it was either in go 1. 2 or 1.3). Granted, it was a library issue (terminfo/ncurse, I can't remember which), but still.
I'm not a huge C proponent anymore (except as a learning language), but C is absolutely more stable than most languages, especially if you account for external libs.
However, the code you need it to explain have to be small and self-contained. When it's part of an actual project, preparing a proper prompt requires a bit of work.
Which might or might not be correct.
https://doc.rust-lang.org/book/
(do the exercises!)
plus a little bit of:
https://doc.rust-lang.org/cargo/
and
https://doc.rust-lang.org/std/index.html
(There's no need to remember the last URL -- just google "rust xxx" and you will get the right page.)
I'm looking forward to reading this:
https://nnethercote.github.io/perf-book/introduction.html
Sprinkle some blog posts on top:
https://xxchan.me/cs/2023/02/17/optimize-rust-comptime-en.ht...
https://matklad.github.io/2021/05/31/how-to-test.html
https://matklad.github.io/2021/08/22/large-rust-workspaces.h...
https://fasterthanli.me/articles/a-half-hour-to-learn-rust
https://fasterthanli.me/articles/working-with-strings-in-rus...
... and the rest is just a matter of applying enough sweat :)
These articles and this general notion that the two has some relevance to each other is just plain wrong and harmful.
Which, by the way, I wouldn't really use go for unless it's a CLI for a daemon/remote program.
I guess Rust can be used for the rest but imho you won't use rust's advantages. One of my favorites being memory manipulation. Rust truly shine for low-level programming imho, but more power to you if you're using it as your generalist language.
Especially web use is one of the biggest places you want to be able to rely on the compiler, aided by patterns like the newtype pattern and the typestate patterns, to catch as many potential DoS or security bugs as possible.
I use Rust as "Haskell, but without having to put up with writing Haskell". (Or, more commonly, I say that Rust is the sweet spot where you've taken pretty much everything functional programming has to offer before you start falling off a return-on-investment cliff.)
Mentioning Haskell within the same enumeration that contains Java and C#. Blasphemy!
Meanwhile, I'm here considering Common Lisp :)
Not being able to generate a statically compiled binary is an issue though...
Some other Lisps may too.
No, it would depend on host libc
Anyway, I'm almost sure I've read multiple times on HN that SBCL can do it, in some threads about Lisp, including recent ones.
If there was a NewGo with no concept of a raw 'nil' value (replaced by Maybe<T>) and err, res in the standard library replaced with Result<T, E>, I'd have vanishingly few reasons to use Rust.
If we got TinyNewGo too for embedded/WASM… uh oh
In that sense, Go already has sum types with how it handles interfaces, but having to check every single method instead of a single trait. In Rust trait solving can only affect compile time, in Go it affects runtime [1]. Anyone who has looked into optimizing Go performance enough has seen what a horror this can be, and it's hard to watch people say that Go's type system is one of its strengths.
Really, most of the people speculating on why Rust compiles are slow simply haven't read how much analysis has already gone into it [2]. We already know the majority of why they're slow: Rust generates a lot of LLVM IR and LLVM has to process that as-is before it can eliminate the redundant parts and optimize the rest.
A complementary issue is that if you use a lot of proc macros, their code has to run before the rest of the compile can even begin, and they often emit a lot of code that becomes a lot of LLVM IR. There are workarounds for both parts -- you can optimize the proc macro compilation itself, and you can keep proc macro code (serde schemas, etc) to a separate crate in the same workspace so they are cached for more of the time. These remain real issues with real tradeoffs, but either way, this has absolutely nothing to do with how long type checking itself takes. If anything, it seems pretty quick given how much macro-generated code it has to check.
The easiest way to see that is to run cargo clippy instead of cargo build. Even despite the enormous breadth & depth of analysis that clippy does -- far beyond simply checking if types are satisfied, and worlds beyond what golangci-lint can analyze -- it doesn't take that much longer than golangci-lint alone. That's bearing in mind it still has to expand the proc macros too.
Finally, Go does a good job at transparently caching a lot of its build artifacts, much more than Cargo/Rust. If you completely clear your compile cache, you'll notice it can take a lot longer in Go as well. I'm not saying that's not an advantage to Go in practice, just that -- again -- it has nothing to do with the type system.
I urge people to speculate less about what makes Rust compiles take longer and catch up on the analysis already done. Even if you don't care about Rust, at least you'll have a better idea of what tradeoffs Go can explore in future, instead of simply assuming that any deviation from what Go does today would make compile times "skyrocket". That's not a useful way to reason about language and toolchain tradeoffs, especially not when real data are never more than a web search away.
[1] https://planetscale.com/blog/generics-can-make-your-go-code-...
[2] https://www.pingcap.com/blog/reasons-rust-compiles-slowly/
GHC for Haskell and rustc for Rust are slower because of the optimization passes that they do. GHC needs to be more aggressive about optimization in order to make lazy code perform well. And rustc in order to deliver on Rust’s promise of zero-cost abstractions.
Just write down how to do good functional programming with currying, HOC right in chapter 1 and all is good. Don't treat reader as "zero-knowledge beginner".
// x and y are captured from the environment
| a | func_that_takes_3_args(x, y, a)