Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Rust vs. Go in 2023 (bitfieldconsulting.com)
90 points by saikatsg on Aug 13, 2023 | hide | past | favorite | 204 comments


Whenever I see comparisons between Rust and Go, I'm always reminded of fasterthanlime's I want off Mr Golang's wild ride [0] as well as its sequel Lies we tell ourselves to keep using Golang [1].

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...


Infact... Early stages of a project should make heavy use of owned and cloned types while you figure out the bottlenecks and work out how to architect the solution.


I don't get paid to write code in these languages; my enthusiasm in exploring them is limited by the amount of energy I have over the weekend. Honestly, I don't see Rust as anything but a code-rewrite language.

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


Like someone else has said, I hear and understand what you're saying but I could not disagree more.

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.


Hard agree here. Once people understand rust, not read some bullet points on a blog they start to understand how useful even the tooling is.

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.


Not really. I use unwrap and clone everywhere and I can do everything as if Rust were then a garbage collected language, and I can then test out different solutions, hack my way through, just as I would in Go or Python. Then once I have a good solution, even if I don't optimize it, it is leagues faster than Python and even faster than Go. If I do decide to optimize it, then, it's fast, really fast.

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.


> Not really. I use unwrap and clone everywhere and I can do everything as if Rust were then a garbage collected language, and I can then test out different solutions, hack my way through, just as I would in Go or Python.

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.


The thing about clone and to a lesser extent unwrap is that 90% of the time, just leaving in the clone is fine. It might be suboptimal, but if it's not in the main loop, who cares?

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.


If that’s the case, then Scala and similar should also enter the question. That is at least an expressive language compared to Go, and if you don’t need rust’s performance it may be a good tradeoff.


While I hear you and grok+understand you; I think this 100% comes from your background and what problems you are trying to solve. As someone that does a ton of systems (traditional definition; e.g. embedded, OS, emudev, etc) development in my hobby time, Rust (and Zig, more often these days [for me]) is perfectly conducive to my use cases and improves my development flow; while using Go would get in the way far too often.

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.


I disagree here. Anvd also disagree with grandparent.

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.


My first (and so far only) Rust program was an 8088 disassembler. Rust made it easy for me to try "different solutions, continuously improve, and write hacky stuff".

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.


Now try that on some domain you're not familiar with...

If you ever have to change lifetimes of existing functions/structs on a non-trivial Rust code base, you're screwed.


I haven't really had to use lifetime annotations yet, but again, they are just a formalization of the way I already think.

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.


Observation: Lifetimes are Rust’s checked exceptions in that regard. Changes propagate in the wrong direction - from leafs of the import tree down to the root. Solutions though… can’t really imagine one that makes sense.


You probably shouldn't deal with lifetimes outside of first order use cases in 9/10 cases when a project is in its early stages. A clone or wrapper type isn't all that expensive in many applications. Just because something can be optimized doesn't mean it should be, especially early on in a project.


*nod* Rust's "make costs explicit" design does have a bit of a problem with encouraging premature optimization where something like Python would encourage sloppiness.


I'll take obvious jank over hidden jank any day personally.


I agree... you need discipline either way. It's just easier to wind up not realizing that you don't need to spend so much time on up-front optimization with Rust to produce a perfectly serviceable program. (Especially if you haven't internalized the whole "fearless refactoring" angle.)


We'd probably be good on a team together. Nice to see like like-minded people in here.


If you spent more time with rust your opinion of it would change. It's strength has nothing to do with rewriting software. It's easy to maintain, and pretty fast to write.

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)


I think it's one of those things where you need to drink the coolaid. Once you get over the learning hump and fighting against you, you won't want to go back to languages with poorer and less strict type systems. There a lot of value in letting the compiler do it for you


> 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.

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…


My experience is the opposite. Refactoring Python code is all but impossible. Dynamic types and runtime errors is nightmare fuel for a refactor.

Rust code lets be confident that when I make changes as I explore the problem/solution space that my program will actually work.


Go is more verbose than Java, which you may very well consider overly verbose. The only reason it is between Python and Rust is that the two languages are two extremes of low-high level languages. Go is as expressive as C, which is a negative.


> Go is more verbose than Java

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.


You can look at "if err == nil" and tell what is going on, but who knows which layer of exception handling will catch that "throws".


Which is the only sane thing in most cases. What can you do inside the helper function of a helper function with an incorrect user data? The only sane place to handle it is n levels up, so bubbling up is the correct default. At least rust has a handy macro for it, but go’s error handling is catastrophic, not a proper sum type, nor some form of propagation… it is literally the bad errno from C burned into the language.


In go, you should manually propagate the errors up by returning them. At the n levels up, you handle the error. It's a bit cumbersome during write time but a life saver during read/debug time.


So manually obscuring the control flow/business logic with boilerplate bubbling up is more readable at debug/read time than it being implicitly, trivially there?


In JVM languages it is easy to know what is catching what.

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.


>90s style "if error" check

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.


There have been many discussions on the topic of go’s error handling, the main gripes of people can be summarized as:

- 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.


It's not what errno does in C. errno in C is a thread-local variable which means accessing it is inefficient (it uses thread-local storage, which is much more expensive than a local variable) and you have to do awful things like save errno at the beginning of signal handlers if you call a function in the signal handler that can set errno (basically: any library function).

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.


> errno in C is a thread-local variable which means accessing it is inefficient

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.


Java's ecosystem is moving away from checked exceptions because their "bolted onto the return type as a sidecar" design doesn't compose well with functional-style APIs.

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".)


> 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).


1. Remember that Rust takes a Minimum Viable Product approach to 1.0 releases. There are experiments in adding stack traces in third-party error-handling crates that could eventually lead to additions to the standard library, but we've already gone through at least three iterations (error-chain, failure, anyhow, and possibly eyre) and already have two deprecated methods forever stuck on the standard library Error trait, so they're taking it slowly.

(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.


Try blocks exist in Rust nightly, but given that an IIFE closure gives nearly the same results, there hasn't been a ton of pressure to stabilize them. I wish the team would though.


That's when automated error handling began to be widely supported in early versions of mainstream languages: C++ (1985), Perl (1987), Python (1991), Java (1995). As time passes, I think it's healthy for us to expect new languages to learn from history and launch with more complete feature sets.


'Automated error handling' is a misfeature in a language where correctness matters. Perl and Python can use exceptions and there it makes sense. It makes no sense for a language without garbage collection and frankly the only system where it actually works well is Common Lisp, which has restarts. Without restarts, exceptions are just error codes but with spooky action at a distance.


What does having/not-having a GC have to do with correctness?


It's been said many times that Go feels like a language written by people who stopped paying attention to programming language research after the 1990s, as well as that it was written by people who look at C and think the only things that are less than perfect about it in the modern world are memory-safety and lack of a first-class concurrency story.

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."


> Go is more verbose than Java

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.


IME, Rust is the best language for doing major refactors of your code. You can make a major change, and if it compiles you can be relatively confident that you didn't miss a side effect of your change. So it's an excellent language for continuous improvement.


Kind of funny, the experience I’ve had with go was ripping my hair out trying to get dependencies installed. Cargo was incredibly helpful and made the process painless.

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.


Isn't it just "go mod install" or something? Oh you probably tried it when golang changes where it cached packages. I feel you that was a super crappy time to learn Go.


> 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.

You just described my reasons for switching from Rust to Go better than I ever have.


Rust is great for writing stuff that needs to be stable with little downtime, for anything that needs to change a lot rust is not there yet, it has lots of sharp corners like not being able to make a factory interface that returns another interface (thogh this is coming in a few months), in general it's hard to do non-leaky abstractions in rust ATM, I do think it will get there in a few years once the few sharp corners are gone.

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


I get why you think that, but I can assure you Rust is perfectly fine for experimentation with enough familiarity. It cannot match Python in the short term, but it doesn't take much hacking with Python until I wish I had started with a more maintainable language. It is really easy to introduce bugs in Python code that would be caught instantly by a compiler and the debugging time adds up quickly.


Can you give an example?


> You may have read articles and blog posts aiming to convince you that Go is better than Rust, or vice versa. But that really makes no sense; every programming language represents a set of trade-offs. Each language is optimised for different things, so your choice of language should be determined by what suits you and the problems you want to solve with it.

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.


Yeah, I really felt like this article had a lot of words and yet didn't actually convey much useful information—nor did it convey any information that hasn't been written about at length elsewhere.

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?


> Rust is a little faster, and avoids GC

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.


> In fact for me the only benefits of Go are single binary and startup time.

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.)


At least two reasons I'm not choosing Go:

- 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.

[1] https://github.com/golang/go/issues/50750


> * Rust is a little faster, and avoids GC.

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.


> Is Rust's compile-time GC about something other than performance somehow?

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


GCs in general can batch memory allocations, reallocations and frees.

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.


That is a great insight, thanks. I have always been under the impression that non-GC performance is strictly better under all circumstances, but your example is an obvious counterpoint.


It depends on the workload. If you have some tight loop that's heavy on computations and memory accesses then the borrow checker will allow you get some huge wins by removing allocations and improving memory locality. If instesd you're writing some IO heavy application then the overhead of allocations will be minimal compared to the IO you're doing, so the borrow checker wins will be really minimal, or Rust might even be slower due to the runtime not being built into the language.


> Is Rust's compile-time GC about something other than performance somehow?

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.


> 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.

By copying more?


By structuring code appropriately - for example, in a GC-based system you can create new objects from anywhere, but in Rust it tends to make more sense to follow a RAII-style model where you take advantage of scope to simplify lifetime management.

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.


By building up design habits that lead to things passing the borrow checker on your first try.


> Memory usage: GCs waste memory in exchange for performance. Go's GC doesn't compact (last I checked), so fragments the heap

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.


Go's GC isn't compacting, and might never be, because it made the short-sighted decision that Go pointers can be handed directly to asm or C code.

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.


Yes, you are absolutely correct (and while I was familiar with Java’s GCs and that Go’s are way behind, I wasn’t well read on what exactly Go’s do, so thanks for writing that!), I wasn’t defending Go’s GC, only making a nitpick.


I didn't intend those sentences to be read as connected.


It is much faster for CPU-bound work. In my experience, 2x-5x is the most common range I get. There are many reasons besides just memory allocation and LLVM that Rust code ends up faster.

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.


It's not just a question of on average how much faster it is, it's a question of whether the distribution of latencies matters.


GC is often faster than ownership. All the extra clones are costly.


That's a fair point, although there is a simple solution in Rust (and other RAII languages) to use heap allocation if what you want is moving big chunks of data (so only the pointer is moved) or in case you really want copies, reference counting (Rc/Arc in Rust).


You don't have to clone... Really you don't. If you want fast rust code, learning how to avoid that is probably step 1.


Go’s GC is very naive and not particularly good. In that regard the JVM is far above other platforms.

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).


> Go's goroutines are much easier to use.

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.


Rust is your "last language". It's good for writing Python native modules, PostgreSQL native modules, compiling to WASM, all using the same libraries you love and know already. That is a big deal for me.


I don’t think a “last language” is something to aspire towards.

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.


I'd recommend learning Type Theory (if you need languages – learn HoTT or just read papers on type theory around borrow checking etc.) to understand how Rust type system works, and assembly/LLVM (and again, papers) to understand how it's implemented. Not C.

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.


> Learning C will teach you how rust works under the hood

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)


> It's good for writing Python native modules

distributions are going to love your module! /s


They already need a workflow for it. pyca/cryptography uses Rust for some of its ASN.1 parsing now.

There was a huge kerfuffle about it.

https://blog.yossarian.net/2021/02/28/Weird-architectures-we...


They have hard time keeping up with regular python packages, I don't think Rust will make a difference. I just do "pip install uuid-utils" and get a uuidv7 implementation in my Python.


Nah, pure python are the easiest to package of course. But C ones with an author who knows what he's doing aren't much more painful to package.


Can do the same with C, and it is still being improved. Safety is IMHO a bit overrated.


Until we finally get liability across the industry, then devs will think twice about shipping CVEs into production.


Yeah multiple attempts are done to put liability on open source library developers.

Terrible idea.


Lol "dear free laborers your code must be flawless or you will be sued"... Hmmm sooo... About changing my hobbies...


If your code is not flawless, do you retain your honor? Without honor, is your life worth more than say, a Cobalt miner in Congo? I have spoken. /s


The value of human life in most places is exceedingly low. In the US if a company kills ya due to an OSHA violation the company pays something like 35,000 to OSHA. I imagine that's more than a Congolese cobalt miner, but when you compare it to the cost of say a car, a house, an education, etc. It's pretty sad out there.


Great idea, so far computing has been the exception, it is time to join the grow ups in responsibility how software affects the society.

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.


Do engineers often download projects from university students and then decide that the university student should get no money and all the jail time if the building falls?


Those engineers are the one facing the court, as they failed to obey the requirements posed upon them by the legal system when they did their oath for professional title, and signing their name on the project.


You're avoiding my question. Please re-read and reply accordingly.


If you are interfacing with other languages or running on mobile (as a library not a full app) Rust is the obvious right choice imo. In other situations the choice is not as clear cut.


> Rust has a much stronger type system and the borrow checker. Both make bugs far less likely.

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.


> No study I've seen (quite a few) has ever shown that.

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.


Could you please link to the Typescript study?

> 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.


As having written a lot of Haskell, I can tell you that people do create bugs there as well, (IMO) to a similar degree as in other languages. The choice of languages seems to come down to personal preference, as does the propensity to create bugs (come down to the specific developer). ;)


100%. Every single study always agrees on that: developer skills completely shadow any differences between programming languages - even between categories of programming languages, like dynamic VS static typed-languages.


While it's not a study, https://blog.polybdenum.com/2023/03/05/fixing-the-next-10-00... gives a real-world example of a bug in Go Rust would have prevented.

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.

https://cliffle.com/blog/rust-typestate/


The main advantage to me is more that I don't have to write tests to test type integrity accross functions.


I've never found myself doing that in Go.


I was talking about strongly typed languages, which Go is. That's why I'd rather use go than python at my job (not an option though).


Rust is even more strongly typed than Go, so it reduces the chance of bugs even more. The borrow checker also reduces the chance of bugs. It was originally created just for memory safety, but it turns out that the ownership structures that it enforces are less bug prone.

It's quite similar to how using immutable data structures leads to less bug prone code.


Interesting that he selected "match" as example of a complex Rust feature. To me, "match"/"case" is one of the simplest and cleanest abstractions in programming languages.


Honestly, I've found match more annoying (and I suspect it's more confusing for newcomers), ever since match ergonomics were stabilized. I do see how it helps some simple cases, but it makes things awfully confusing in so many other cases...


That's wild. First time I saw a match case, if let, and matches! Invocation I was freaking relieved. No by hand type checking in a dynamic language, having to consider corner cases, etc. LSP even autocompletes the cases with a single hotkey. Escape from boilerplate hell.


*nod* Aside from the obvious (compile-time guarantees beyond MyPy's ability to offer), the lack of pattern matching and an accompanying ecosystem that evolved in its presence is my number one discomfort when writing Python for things like my QWidget GUI frontends for Rust backends.


I was agonizing over this choice too. Eventually rust won:

* 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


Go is only memory safe for simple sequential code. Most concurrent Go programs share memory among threads/goroutines in an unsafe way.


On the other hand, goroutines makes it very easy to write concurrent code. And the built-in checker for race conditions works well.

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.


That causes concurrency issues, but that is not what is commonly understood by memory safety.

With that said, Go indeed gives you plenty of rope to hang yourself with in relation to concurrency (among others).


Technically, there's memory unsafe memory sharing: Interfaces and slices are mutable and consist of multiple word that are accessed without synchronization, sharing a reference to a interface or slice between goroutines means you can get a mismatch between vtable ptr/data ptr or data ptr/length.


That is again, language-dependent. Java for example can memory-safely have data races, even those that tear (what you are referring to here). Not the standard, but OpenJDK itself also has a guarantee that every value that any thread could observe was actually set by some other thread, so on incrementing a number from 0 from multiple threads you can never get -37, but for surely can observe an older value.


I think this article is a nice summary of the languages, but the opening set up bugs me:

> 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.


Odd. I wish Rust had Django's ecosystem so I could do more Rust web stuff, and I do use Rust for "shell scripts" and basic utility tasks, not for the performance, but for the compile-time guarantees.

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.


At least two reasons I'm not choosing Go:

- 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.

[1] https://github.com/golang/go/issues/50750


The difference/tradeoff shown in the blog fades away in developers psychology as they write more code and ship apps, so I would not held them high.

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 vs Go is like Jeep vs Prius. They both do the same kind of thing, but in such contrary ways that I don’t think anyone is confused which one they want.


After reading many comments here the one that appeals to me is the readability of go code. There was this moment of ruby vs python where I was delighted by the readability of python code and how I could easily read the code of python libs as well. I feel this is the same with go. I can imagine the rust wizards easily making hard to read code.


I've tried Rust for a short while to get a good feel. I've been Go developer for nearly a decade. Unfortunately there's a bit of a forced and misleading Rust favoritism the days but from my experience in various startups, including my current one, Go is by far the best choice for backend development!

- 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!


Having built our startup on Rust with no prior knowledge of it: These are not facts but subjective opinions that might be true for you but not for everyone.

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.


Indeed, my web backend is in Rust and it's rock solid, no crashes even a single time in a few years, simply because Rust looks out for errors a lot more thoroughly than Go and others.

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.


If it works for you it's great, but for vast majority it's the wrong step forward. Biggest enemies of a startup are time and money. If you have both, great but vast majority have neither!

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 make all these claims but have not presented any proof apart from your anecdotes.

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.


Here is an opposing opinion from someone who used Rust in a startup but would not pick it again: https://www.propelauth.com/post/i-love-building-a-startup-in...


> Rust is not suitable if the initial team members are not Rust experts

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.


You are making very strong assertions, some of which I would like to question/temper/gently push back against.

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.


> Unfortunately there's a bit of a forced and misleading Rust favoritism the days

This had to be said, well done! It feels like you get bashed for criticizing Rust just a little nowadays.


>- Rust is not suitable for fast paste startup environment

Freudian slip ... copy pasta environment?


I filed that one under "text to speech or spellcheck mishap" after pondering a bit, myself.


Yes, I thought the same after reading the thread again myself; or maybe, autocorrect error.


I’d hire a Python developer any day of the week over either Go/Rust.

Need some data science work done, easy. Need a web server done, easy. Need things to be fast and relatively performant done, easy.


Having created and maintained python and go web servers, python never again if I have a choice. Web servers live on concurrency. It is also nice knowing what was passed into a function. Caching in memory is much more approachable in go; making that thread safe in python is a chore.

Oh, and performance. Not even close. Go is easily 20x performant in each case we've migrated.


> Python

> 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.


Yeah. The presence or absence of compiled extensions is a HUGE factor. Implementation design is also huge.

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.)


So what? That's how things are structured in Python. It's one of the advantages of the language. Sure it'd be nice to be like Common Lisp, where you can write dynamic/slow and optimised/fast code in the same language, depending on (declare). But Python is the next best thing.

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.


Is it obscuring the logic to explicitly handle the cases where an item may not exist, or to tell an engineer when they are copying memory? In my opinion it's the opposite.


When you want to write some simple test code to solve a problem, then it is absolutely obscuring the logic to write out all the details of memory management.

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.

[1]: https://wiki.c2.com/?PlanToThrowOneAway


I know where you're at because I used to feel the same way. But what if rust was designed to make manual memory management as painless as possible? IE, it's the antithesis of a language like C because when you learn it's syntax all practical memory management is done by basically writing OCAML.

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.


Orders of magnitude slower with few options for improvement except writing in another language in a lot of cases.


Need something maintainable and safe at run-time, not easy.

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.


You're not really talking about Python/Go/Rust developers, but about your preference for Python as a technology.

Also, Python is super slow comparatively and is only fast if it delegates to other languages.


It's a jack of all trades but a master of none. Nothing wrong with that until there is. Then you end up with two languages. This works fine for a while. Until you need 3... Next thing you know half your code was rewritten in other languages and cleaned up. Start realizing ya could have started your projects using different tools.

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".


That's actually one of my reasons for writing in Rust whenever possible.

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.


Good on you! You have to be product/solution focused not tech glitter!

Jobs to be done and the fastest/cheapest way to get there!


Pretty balanced article, but I think it skips over the (for me) main points of using Go:

* 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.


> 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.

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.


Old C projects are very often troublesome to compile on a modern system with modern libraries.

Source: Arch Linux package maintainer for 10+ years


That's a problem with those libraries and not with C. People can make backwards-incompatible API changes to libraries in any language.


Sure, but Go has a good system to avoid this, with go.mod and go.sum.


Go didn't invent package management. You have been able to do this for 20 years in general. go.mod and go.sum help you with go dependencies on go packages. What about Python dependencies on C? Or C dependencies on Go? You can do this in full generality and yet people keep reimplementing it language-specifically because they refuse to just use APT or something.


C no longer has a conservative standards committee. This was true until C23. In practice it doesn't really matter, because compilers will never actually break old code. But the committee is now actively trying to break your code, by doing completely pointless things like replacing _Bool with bool to save you one #include.


Just compiled my 10 year old C project to test this claim. No issues (and it was written in C89, not C99 nor C11).

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.


Heh, I too was surprised to find a similar aged project of mine that failed to compile for very similar reasons. It was a multiplayer terminal game. Nothing fancy, just ascii characters that could navigate an ascii maze and "eat" other characters. Didn't spend any time to dig in. Def let down by the backwards compatibility promise.


> The same is not always the case for C, C++

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


> and possibly even Rust.

This is patently false. Rust has a severe backwards compatibility policy.


What is the recommended resource that helps one become proficient at Rust nowadays?



Just practice. Screw all the "how to learn x" grind culture stuff. Just download it. Build a project with it. Read other people's code. Rinse and repeat.


I would recommend ChatGPT. It’s pretty good at pretending to understand your struggles with rust. You can explain your issues and swear a lot and you will get a nice explanation in return.


Joke apart, yes. ChatGPT can sometimes be more useful than rustc error messages.

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.


> and you will get a nice explanation in return

Which might or might not be correct.


Like everything your read yes.


No, not like everything you read at all.


Most of my complaints against Go are kind of waned, however can we at very least have Pascal like enumerations without iota const dance?


I don't like that iota stuff either.


The article doesn't discuss one of the most significant differences between building in Go and Rust: tooling.


Rust and Go have almost no relation to each other - one is a low-level language, like C, C++, the other is a high-level one like Java, C#, Haskell.

These articles and this general notion that the two has some relevance to each other is just plain wrong and harmful.


There are definitely relations between them. They are often used in the same domains. Servers, DevOps, CLI, tooling ect. Just because one has manual memory management and other is GC doesn't mean they aren't relatable.


The only 'natural' niche for Rust in your list is CLI.

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.


I think they're all natural niches, because Rust's performance and ability to tightly control memory allocation are over-promoted and the strength of the type system as a means of ensuring correctness at compile time is underpromoted.

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.)


Python is also used in those niches, let’s add that also to the list.


Rust is a low-level language with high level abstractions, while Go is a high level language with low level abstractions.


> Java, C#, Haskell

Mentioning Haskell within the same enumeration that contains Java and C#. Blasphemy!


I feel you are being sarcastic, but for others: I put it in the same basket to widen people’s perspectives — it has a lean runtime with a GC within an AOT compiled single binary with quite great performance!


When it comes to blasphemy, logic does not apply ;)


>Which is better, Rust or Go? Go or Rust? Which language should you choose for your next project in 2023, and why?

Meanwhile, I'm here considering Common Lisp :)

Not being able to generate a statically compiled binary is an issue though...


If you aren't nailed down on lisp but scheme would be an option for you, there's cyclone [1] which also has FFI and C ABI support iirc.

[1] https://github.com/justinethier/cyclone


I've read that SBCL can.

Some other Lisps may too.


> I've read that SBCL can.

No, it would depend on host libc


Why? Something to do with static or dynamic linking?

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.


Go needs more syntactic sugar.


I honestly think the only remaining thing I'd need from Go to almost never pick up Rust for a new project is sum types.

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


If Go had sum types it would be a completely different language - compile times would skyrocket and it would likely look like Rust without the borrow checker. Personally I think it would be better than Go, but I think most Go folks would dislike it. For me the big draw of Go (which are few) is the compile time.


What makes you think that sum types make compile time skyrocket?


Because it introduces a different type system like Hendly-Milner. One of the reasons Go has incredible compile times is because it doesn’t have to calculate types the way Haskell, Rust and other ML-like languages do.


OCaml itself is known for exceptionally quick compilation though. Despite Hindley–Milner with type inference.

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.


The fact that cargo check (which does full type checking) is so much faster than cargo build proves that type checking doesn’t account for the bulk of compile time.


Go already faces a subset of this type system challenge in an arguably much worse way: interfaces are implicitly implemented by any type with the corresponding set of method signatures, unlike Rust where traits are implemented explicitly.

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/


[flagged]


Would be nice to add a short disclaimer that you are the author of the V language (at least it seems from your username, correct me if I'm wrong).

There are many new languages in the same space (Zig, Nim, V). However, if you use a language outside hobby projects, you are dependent on a language having a long-term healthy ecosystem. Of the newer native languages, only Rust, Go, and Swift have enough traction to bet your livelihood on.

In the end, having a rich ecosystem with good libraries and tools is going to leave a far bigger mark on productivity than relatively small differences between Rust, V, Zig, Nim, D, or whatever.


True, Go has an amazing ecosystem. A great stdlib and libraries.

That's why we're working on Go2V, which will be able to fully translate Go code to V. (Having a language so similar to Go also helps.)

It's already been partially used in our translation of Go's crypto modules:

https://github.com/vlang/v/tree/master/vlib/crypto


Spent a few months writing a pretty hefty Cqrs app in Go last year, had a fun but pretty unintuitive experience with rustlingss, and V really does seem like a cool in between! I look forward to taking it for a spin


So V also has nothing to do with rust, the same way go also hasn’t. Both are high level languages, of which we have 1000s to choose, leaving rust and zig be modern low level languages that are no longer experimental/research.


What kind of low level stuff can you do in rust/zig that you can't in V?

V works just fine for low level stuff, we have a full os/kernel written in V, which can already run g++ and doom.


Microsoft has a whole OS written in C#, lisp machines were also a thing, so nothing new. GC is not a problem for OSs.

But there are very few applications that do mandate a non-GCd language, like writing performant runtimes, deterministic workloads, etc.


So what kind of low level stuff can you do in rust/zig that you can't in V?

You don't have to use GC in V. It has optional manual management/autofree/arena.


>(no err != nil checks),

How does that work in practice? Example please.


V uses Result types for handling errors, and the developer must handle them by propagating the error or breaking/returning or panicing.

For example:

  // os.v
  fn open(path string) !File {}

  // main.v
  file := os.open('out.txt') or { panic('failed to open: $err') }
  file := os.open('out.txt') or { return }
  file := os.open('out.txt')! // propagate the error
  if file := os.open('out.txt') {} // alternative scoped syntax


Thanks.

V syntax looks somewhat clear.


Love the hustle. :D


The fact that, in the official Rust Book, the author even don't know how to write Higher order function in Chapter 1 said something about Rust.

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".


Why would you want currying? It is one of least useful/most dumb features of old-skool applicative languages. It is often combined with a really dumb "no parentheses" attitude to arguments. If you really need something akin to currying (which you rarely do), why not use a closure as a wrapper?

    // x and y are captured from the environment
    | a | func_that_takes_3_args(x, y, a)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: