I could see migrating from C or C++ or Python to Rust, for various reasons, but for web back-end work Go is a good match. I write almost entirely in Rust, but the last time I had to do something web server side in Rust, I now wish I'd used Go.
The OP points out the wordyness of Go's error syntax. That's a good point. Rust started with the same problem, and added the "?" syntax, which just does a return with an error value on errors. Most Go error handling is exactly that, written out. Rust lacks a uniform error type. Rust has three main error systems (io::Error, thiserror, and anyhow), which is a pain when you have to pass them upward through a chain of calls.
(There are a number of things which tend to be left out of new languages and are a pain to retrofit, because there will be nearly identical but incompatible versions. Constant types. Boolean types. Error types. Multidimensional array types. Vector and matrix types of size 2, 3, and 4 with their usual operations. If those are not standardized early, programs will spend much time fussing with multiple representations of the same thing. Except for error handling, these issues do not affect web dev much, but they are a huge pain for numerical work, graphics, and modeling, where standard operations are applied to arrays of numbers.)
Go has two main advantages for web services. First, goroutines, as the OP points out.
Second, libraries, which the OP doesn't mention much. Go has libraries for most of the things a web service might need, and they are the ones Google uses internally. So they've survived in very heavily used environments. Even the obscure cases are heavily used. This is not true of Rust's crates, which are less mature and often don't have formal QA support.
I love Go and used to write it heavily for anything non LLM based.
Now that we have agentic coding I just write everything in Rust and couldn’t be happier. The struggle with rust was writing it, go was made so it was easy to write for mid level engineers. Now that we have agentic coding I’m not sure Go’s value prop holds up anymore
My rust services have been nothing short of amazing from a performance and reliability perspective
For me the bottleneck now is reading/reviewing code, not writing code. As you said, AI makes it way easier to write, but do you not review the code? And isn't a verbose, cryptic language with lots of nitty gritty memory management not harder to read/review?
I'm not sold on Rust being a great language to use with AI unless the reason to use it is a lot more than just Rust being fashionable.
It's the same logic for human and for AI code: In Rust the compiler catches many bugs so you don't have to.
If the LLM gives you safe code you know there are entire classes of things you don't have to review for.
That said, I agree with you. My experience is that LLMs are great if you are highly competent in the domain in which you let them work. And it's probably easier to be competent in Go than in Rust.
Rust has practically one error, it's the Error trait. The things you've listed are some common ways to use it, but you're entirely fine with just Box<dyn Error> (which is basically what anyhow::Error is) and similar.
They all convert seamlessly, and the enums make the branches explicit. Don't even need to check the documentation to find which errors supposedly exists like in Go with its errors.Is, errors.As, wrapping and what not.
An easy rule before you make a knowledge based choice is Thiserror for libraries, helping you create the standard library error types and Anyhow for applications, easy strings you bubble up.
Or just go with anyhow until you find a need for something else.
I’ve repeatedly tried using Rust and the error handling has tripped me up every time and has been ~90% of the reason for moving a project back to another language. I’m sure I’m just holding it wrong, but what I run into usually goes something like this (mind you, I have read the Rust book):
* Someone tells me to use enums for errors, in a comment like yours
* I try writing the enums by hand, implementing the error trait
* I realize that in order to use the ? operator I need to implement From on my errors (I’ve read so many comments about how awfully verbose Go errors are, so I assume I’m supposed to use ? in Rust). There are also some other traits IIRC but I’ve forgotten them.
* I realize that this is pretty tedious, manual work, so someone points me to thiserr or similar
* Now I’m debugging macro expansion errors and spending approximately the same amount of time
* I ask around and someone tells me not to bother with thiserr and to just write the boilerplate myself or else to use anyhow or boxed errors everywhere
* I try using boxed errors everywhere, which works, but now I have all of these allocations which feels like I’m doing something that will bite me later. Oh well, but now I need to annotate my errors so I can figure out what is actually happening. I guess I should use anyhow for this?
* Anyhow mostly works but this is approximately as verbose as the Go error handling that I’m told is Very Bad, and when I ask for code review most Rust people are telling me not to use anyhow because errors should be enums, at least in the API surface
I’m sure I’m doing it wrong, but as with many things in Rust, the Right Way is so rarely clear and every other Rust person gives different advice about how to solve my problem and the only thing they seem to agree on is that Rust has an easy solution and that I’m following the wrong advice. (Similarly when I had lifetime problems and half the community told me to just use clone and Rc everywhere until I had performance problems, so instead I just had different static analysis problems).
I don’t love Go’s error handling. It feels like there has to be something better than its runtime-typing. But it largely gets out of the way—creating an error is just implementing the Error method, and if you need a concrete type you use Is/As/AsType. Wrapping is fmt.Errorf. All of this is built into the stdlib and used pretty ubiquitously across the ecosystem—I don’t run into “this dependency uses a different error framework”. Error handling is marginally more verbose than with Rust if you are actually attaching context in both, and neither solves the problem of which call frame attaches the context about specific function parameters (e.g., which level of error context specifies that the function was called with path “/foo/bar.baz”). It’s terrible, but it works—feels like the least bad thing until the Rust community can arrive at some consensus and document it in The Book. Or maybe I just need to try again in the LLM era?
Anything other than panic/abort on allocation failure is outside the scope of the vast majority of programs, including anything using the standard library in Rust. I wouldn't worry about Box<dyn Error>.
Do you really want that data passed back down to the caller of the allocation? From the description of the failure state you'd want to log that data instead: what's the caller of the allocation going to do if you tell it it failed with a crazy size? It already knows the size, it's the one who asked for it.
So, suppose it's a rust library -- you're locking me into whatever logging system the library author chooses? Maybe I'd like to consume the relevant data at the entry point and send it to a logging system of my choice.
A Rust library likely wouldn't be returning an opaque Box<dyn Error> to begin with. Errors are part of a library's API—it's what allows consumers to handle them—so you'd define an enum of possible errors your library could produce and return that, which would be stored on the stack.
usually “stdout” is good enough, wrapper/runner routes output to logserver for collation and search. who cares about formats as long as it’s reasonably structured and searchable?
You're already writing Rust in a very different style if you're writing the type of code that gracefully handles allocation failure. It's to Rust's immense credit that this type of coding is actually fairly well-supported (unlike in Go), but you're already a bit off the beaten path for stuff like error handling.
For me the main advantage of Go over Rust is compilation speed. Then compared with Go Rust still rely on many C and C++ libraries making it problematic to cross-compile or generate reproducible builds or static binaries.
The minus side of Go is too simplistic GC. When latency spikes hit, there are little options to address them besides painful rewrite.
I've run into GC pauses, I think in many (most?) cases there is some class of bulky data that you can either move into slices of pointer-free structs (so the GC doesn't scan them) or off-heap entirely. The workload where GC is slow is also likely prone to fragmentation so whatever the language you'll have to deal with it.
Java with its copying GC deals fine with fragmentation albeit at the cost of more upfront memory. And even in Rust one can change the allocator to try to deal with fragmentation. But with Go there is simply no good options besides the rewrite.
Isn’t it somewhat easy to remove allocations in Go? I haven’t had to “rewrite” as such, but rather lifting some allocation out of loop. Am I misunderstanding the scenario?
With backend serving many clients with widely varying performance profile of individual requests when latency spikes happen there is no particular hot loop. Just many go routines each doing reasonable thing but with a particular request pattern hitting pathological case of GC.
I agree! The line early on about this being for backend services caught my attention. I love the Rust language and use it for embedded firmware and PC applications, but still use Python for web backends, because Rust doesn't have any tool sets on the tier of Django (Or Rails). It has Flask analogs, without the robust Flask ecosystem. I have less experience with Go, but would choose it over Rust for web backends, for the same reason you highlight: The library (including framework) ecosystem. I am also not the biggest Async Rust fan for the standard reasons (The rust web ecosystem is almost fully Async-required).
Conversely, the Go community tends to actively shun frameworks, especially anything Rails-like, and will tell you to just use the standard library. Which is good advice, the standard library really does have everything you need. But it's also roughly on a par with what's available in Rust (though as someone said above, the Go stdlib routines have been heavily, massively, tested in production by now, and are fully mature and load-bearing).
Interesting! Are Go backend building custom auth, admin, DB ORM/migrations/auto migrations, templates, email, dev server etc for each project? Or each person and org has their own toolkit they use?
There are various libraries people use for auth, etc. But rolling your own isn't hard - Go has (e.g.) bcrypt in the standard library, so most of the heavy lifting is already done, you can write a solid auth implementation in <50 lines of code using that.
Generally Go prefers libraries to frameworks. Wrap the hard bits up into a library that can then be used widely in any implementation, rather than rolling it into a one-size-fits-all implementation that doesn't really suit anyone properly.
“we” are all different and i can tell you from experience that there are also many people and teams who use go and prefer ORMs and frameworks and do not build everything from scratch …
For me going from JVM and CLR ecosystem of programming languages into Go for backend development is a downgrade.
The language design makes sense in the context of Oberon (1987), and Limbo (1995).
Now when there are so many options finally building on top of Standard ML, and Lisp heritage, having to settle with Go feels like a downgrade.
I code since 1986, if I wanted if boilerplate error handling, or having cost as the only mechanism to declare constant values, there have been plenty of options.
…the "dataset_id" path variable is parsed straight into the dataset_id arg, and a query string "verbose" is parsed into a boolean. Super convenient compared to Go, and you type validation along with it.
Many other things to like: The absence of context.Context, the fact that handlers can just return the response data, etc.
I'm not sure if that's a great example. What kind of errors could ShouldBindQuery return?
I would assume Axum returns a bad request error for you when query parsing fails, but if you do want more control over how the error is handled, you can change the parameter type to Result<Query<bool>, QueryRejection>, and the type system itself documents precisely what errors you can match against.[0]
Rust does not have three error systems. It has one: the Error trait. io::Error is one of many that implement it (nothing special about it). Errors defined via thiserror also implement it.
“Anyhow” just allows you to conveniently say “some Error” if you don’t care to write out an API contract specifying types of errors your function might spit out.
Not sure what you mean by that. If you're consuming the API of a crate that has functions that return errors, you're not really dealing with a "framework", you're just dealing with whatever the `E` is in the `Result<T, E>`. If that `E` doesn't implement std::error::Error, I'd consider that a deficiency (even a bug) for that crate. (Yes, I know some crates want to support use in `no_std` environments; that's what features are for.)
If I care about the specific variants of error that a function can return, so I can do different things depending on what kind of error occurred, I'll read the docs and match. That's not really a "framework" thing; that's just a basic thing that anyone has to do in any language in order to consume an API. If I need to propagate the error, I'll do so (either directly, or by wrapping it in a variant of my own error type). I don't see how any of this is "framework"-y.
A crate's decision to use thiserror (or not) does not matter to me. If a crate exposes `anyhow::Error`, that's a lazy choice and bad API design, but still "works" and I generally don't need to care about it.
Or is there something else you meant when you said "error frameworks"?
I guess you might have to if you need to use a library someone's written that doesn't implement the standard.
Writing primarily applications, I couldn't tell you what error handling frameworks my dependencies are using: I literally don't know, and haven't needed to know in order to display, fail, or succeed.
EDIT to add: I use anyhow for this, so I should also add "add context to an error when I fall" to the list of things I do.
What's the standard? I'm not being snarky; I'm going down the thought process of how this would work in practice.
I am on team Io Error [on std rust]", somewhat arbitrarily. If I call a lib that is on Team Anyhow, or Team Custom Error Enum, I will have to do some (Straightfoward, but a little clumsy) conversions if I want ? to work. This is complicated by being able to impl From<ErrorType1> for ErrorType2 only in one direction if you don't control the other crate. (due to the orphan rule)
By standard I meant an error type that implements std::error::Error.
EDIT: Which I assume all my dependencies have done, given that anyhow is able to consume all of them.
I specifically called out writing applications as my use case: my only objection to tptacek's note is the somewhat universal "in practice". The burden for designing errors for a library that others will use is higher, but that's far from the default/universal experience.
Many more people are going to consume libraries & not produce any of their own, and I think my experience is representative there.
Not rust specific, and most certainly not a criticism of you - but I hate when people call a lib that errors, then just bubble that error up.
I mean the error is supposed to be tailored to the audience - I guess what you are saying is that you handle the error by saying "I called foo with X, Y, Z, and got this error back" in the logs - which your caller then also does - producing a log message of
ERROR: I called Foo with X Y and Z and got error: Die MF die
followed by
ERROR: I called Bar with X Y Z and a and got error: ERROR: I called Foo with X Y and Z and got error: Die MF die mf (still fool)
And so on and so forth.
If the counter is - don't log, that's fine, but you have to know where in the call graph that error state was reported to the logs
I have tried to figure out some kind of unification between "collecting error state in a function", "logging error state", and "return error state to a parent".
I haven't found any satisfying solution to it all; collecting information for logging vs information that a caller would want... I've been meaning to investigate tracing_error to see if it brings it all together.
Regardless of language - if you find a good, clear answer - blog the hang out of it - I for one have been searching for the right way to manage this, and it's not (yet) clearer - other than what I've said so far
Yeah - but that's the same as my final point - you have to know who is supposed to manage the error/log - all the way up (and down) the call graph
edit: I've just finished debugging a multi system chain - FE -> SNS -> SQS -> Lambda -> DynamoDB -> Lambda -> Webhook -> My poor code
My code has multiple layers - and I was trying to find where in the very long chain of calls the data was being mangled
It turned out that there was an unlogged error, which was mismanaged by a caller - there's no shade here - the caller was handling the error how it was designed to, but by not logging that there was an error - it took a minute to understand.
I was a big fan of go for a while. Though now that I have programmed more swift and rust recently, having a compiler that doesn’t protect against null pointer deferences or provide concurrency safety guarantees feels a little prehistoric.
Though go certainly did a much better job than rust on the standard library front.
Standard library is something you have to maintain for all eternity, with identical API. It had been argued that some concurrency primitives like channels would have been better outside of std (for rust, to be clear). Once dependency management is solved, a small std is beneficial.
> you have to maintain for all eternity, with identical API
People always tout this as a huge reason for not wanting a too big std in Rust (or "too useful" either), but IMHO that's just talking about reaching theoretical optimals, while leaving the community for years without good guidance via providing a opinionated practical and pragmatic way of doing things. Which I find to be a very unhelpful stance for a tool such as a programming language.
If a design of some std package didn't pass the test of time, and a new iteration would be beneficial, the language can leave its original API version right there, and evolve with a v2, with an improved and better thought out API after learning from the mistakes of v1.
Prime example: "hey we found that math/rand had some flaws, so here is math/rand/v2". A practical solution, and zero dramas as a result of having rand be part of std.
Perhaps it would help if stdlibs were be versioned, with the chosen version declared in the project file. For existing languages, a lack of version would simply indicate the original stdlib, meaning nothing should break.
I definitely don't think stdlibs should be changed often, but it seems fairly damaging to a language when things may be added to a stdlib but never removed, no matter how broken or misconceived (see C++).
Rust is a great language, but the poor stdlib + overreliance on crates + explosion of unvetted transient dependencies makes it a hard sell for a lot of projects.
I use go because of the large and useful stdlib. I rarely have to reach for an external library, and even then I only consider libraries that are very popular. If a library isn’t available, I’ll just write my own, using the stdlib. I recently used the awesome crypto library to implement an envelope encryption system, I didn’t need anything outside of the stdlib or Google’s x library (x is effectively the experimental stdlib).
Having too much external code, like npm or rust crates, seems like a nightmare for me.
It's not that bad, but when compared to C or Go, it's not something that I would like to type by hand. At least Java has IDEs which reduce the amount of verbosity you need to type. I know you get safety, but the verbosity and cargo is is a con in my opinion.
thiserror and anyhow are just std::error with extra steps. Note that io::error is just a specific std::error.
The entire point in Rust is that you wrap Error impls with other Error impls, or translate one impl into another using a match. I've found this is far more flexible and verifiable than most other languages, because if you craft your error types with enough rigor, you can basically have a complete semantic backtrace without the overhead of a real backtrace.
I use thiserror a lot to help with my impls. Notably, all it does is impl Display and Error. It's not a specific other paradigm because it basically compiles out, it's just a macro.
Anyhow is perhaps the closest one to another paradigm because it allows you to discard typed information in favor of just the string messages, but it still integrates well with Errors (and is one).
I find Elixir's memory and threading models much more compelling than Go's for web services. There are many great libraries for Elixir as well, but if you need something else, Elixir makes rolling your own libraries very easy. I'd recommend giving Elixir a try, if you haven't already.
Praising go for how it handles errors, when it's even worse than C where the compiler at least warns you if you're ignoring return values of calls. That's a new one.
Linters are available to catch you before you compile - with Go
Generally speaking there has to be a mechanism for optional handling of return values, in Go you can ignore everything (ew), you can use placeholders `_`, or you can explicitly handle things - my preference.
If you say "Well in C you have to handle the returns - I am not across C enough to comment, but I will ask you - Does C actually force you, or does it allow you to say "ok I will put some variables in to catch the returns, but I will never actually use those variables" - because that's very much the same as Go with the placeholder approach
edit: I am told the following is possible in C
trySomething(); // Assumes that the author of trySomething has not annotated the function as a `nodiscard`
(void)trySomething(); // Casts the return(s) to void, telling the compiler to ignore the non-handling
int dummy = trySomething(); // assign to a variable that's never used again
C, as a language, cannot bother less about you using or not using the return values, checking them, discarding them, or using them to index an array without any bounds checking. Various linters and compilers may have their opinions, expressed as warnings, but at the end of the day it's completely up to you as a developer.
This is a weird document that is simultaneously trying to serve as a migration guide and an advocacy document for Rust.
Ultimately, if you have to ask, the Rust vs. Go consideration boils down almost completely to "do you want a managed runtime or not". A generation of Rust programmers has convinced itself that "managed runtime" is bad, that not having one is an important feature. But that's obviously false: there are more programming domains where you want a managed runtime than ones where you don't.
That's not an argument for defaulting to Go in all those cases! There are plenty of subjective reasons to prefer Rust. I miss `match` when I write Go (I do not miss tokio and async Rust, though). They're both perfectly legitimate choices in virtually any case where you don't have to distort the problem space to fit them in (ie: trying to write a Go LKM would be a weird move).
The Rust vs. Go slapfight is a weird and cringe backwater of our field. Huge portions of the industry are happily building entire systems in Python or Node, and smirking at the weirdos arguing over which statically typed compiled language to use. Python vs. (Rust|Go) is a real question. Rust vs. Go isn't.
Exactly. 95% of programmers are application programmers - they ship software used by regular users. I think it's insane to use a non-GC language for most of those cases. Manual memory management is mentally taxing and it's easy to make catastrophic mistakes. The marginal benefit from it is just not worth it unless you're making games or a trading system.
5% who write tools or other "infra" layer for the other 95% to work on top of maybe need that level of control over memory. It doesn't make any sense to me to sign up for that complexity unless you really really need it.
Maybe I'm misunderstanding something but non-GC language doesn't mean you have to do memory management manually? I mean, for example, in Rust (or modern C++), it's basically automatic. There is no mental tax or catastrophic mistakes as far as I know.
If youre not writing the code yourself and vibing away which I think most people generally are despite the disdain around here then why would you not choose the "more performant language" (I know that isnt necessarily reality but it is a common perception).
Go's managed runtime is less valuable when the LLM is perfectly happy to slap a bunch of stuff together for you to and approximate it and doesn't complain at all when writing async rust despite some of the rough edges.
Correction: The use of LLMs has caused every major language usage to explode.
And as mentioned in other comments, Rust slow compilation can be detrimental to LLMs + fast iteration speed. And it's not just speed, Tauri takes 20GB of disk space to compile. It's bonkers. This is npm/js ecosystem all over again but slower.
Another reason to pick Go if you're leaning on LLMs is the standard library. Often you can do more work with fewer dependencies.
I'd rather leverage world class engineers paid by Google to maintain dependencies for me than try my luck with half a dozen of 0.x crates. Plus stdlib APIs can (and are) versioned just like third party dependencies.
Fully agree with this. We use Rust in an enterprise setting for building web app backends and the experience is painful. A lot of crates just seem like someones side project. Too many ways to do things leads to bike shedding in PRs. Compile times are atrocious and can take like 30 mins to build.
Honestly using Go would have got us to the same point much quicker, with code that is much easier to review.
I like vibe coding but I am sceptical that a vibe coded runtime in Rust would be as awesome as the Go runtime which is written with deep expertise of Unix software and threading and many low level details that are subtle and do depend on global properties of the code to work flawlessly. It makes sense you can crank out Rust with an LLM if you know what you are doing, but if you want a GC type thing or preemptive scheduling across an N by M threading model, then you are competing against some very good code.
I agree that agents make Rust a lot more tenable for less "kernel-and-browser"-demanding tasks than it was 4 years ago, but I do not agree that they eliminate the "managed vs. unmanaged runtime" question, and to the extent they influence any of this decisionmaking at all, you have to accept the notion of not reading the code. If you're reading it, it matters that Rust makes you do bookkeeping that managed runtimes avoid.
> The use of LLMs has caused Rust usage to explode.
Rust had a "vibey" community long before vibecoding. In particular, it's long been fairly non-serious about yolo importing a bunch of crates to solve things (since the standard lib is small) which is kinda the same problem as having all those things just vibecoded. Either way, most projects weren't reading all of that other code!
> the Rust vs. Go consideration boils down almost completely to "do you want a managed runtime or not".
That's not really something I care much about. My beefs with Go are 90% about the syntax of the language itself, and it's weak (compared to Rust) type system.
When it comes to a managed runtime, for most tasks, I generally don't care if my language has one or not. For some tasks I do, but there are not many of those tasks, and so this question is mostly irrelevant to me when deciding Go vs. Rust.
I don't really get where you're seeing that the predominant Go vs. Rust debate is about the runtime. IME it's the subjective stuff about the languages themselves, and their ecosystems and communities.
> The Rust vs. Go slapfight is a weird and cringe backwater of our field.
::shrug:: I dunno, I mostly stay out of it and just use Rust, and I'm happy and avoid the drama. I've written a little Go here and there, didn't really like it, and moved on.
I think people do this for every language. It becomes a part of their identity, and then they have to defend it. I used to do that too, long ago, but I don't have the time or energy for it for the most part, and find it boring, so that $LANG-user-as-identity bit of my has fallen by the wayside.
I don't think it's about adoption levels; sure Go and Rust are tiny compared to JS/python/etc. It's emotional, not about who has the most users or who can even plausibly get there.
Right, so you don't need a large portion of Go's runtime benefits, because you have a far better version of it already, zero cost abstractions and TRUE memory safety, not pretend memory safety behind a -race detector with zero compiler guarantees...
I think I'd be ok with node via purescript? But in general I think rust and go people should join forces against the evils of dynamic typing. Isn't type hinting finally considered best practice now? I think that is effectively an admission that it was a defect. And even with good ginting it is still worse than inference. Inference can let plenty of code go untouched on type changes, while still protecting against unindended type changes.
Us Node folks adapted typescript because we wanted static compiled types.
I wish TS had more of a runtime. The only thing I'm jealous of with regards to python is how seamlessly you can do JSON schema enforcement on HTTP endpoints. The Zod hoops are a constant source of irritation that only exists because the TS team is dogmatic.
I think Typescript is a perfectly cromulent language. I don't know it well but would seriously consider it for any problem that had a shape that admitted a dynamic language. There's a lot to be said for using dynamic languages, too!
This is probably going to sound generic / repetitive, but my biggest complaint about Rust is the package management situation, which is entirely the result of the developer mindset. I love the ergonomics on the rust side (the functional approach to data types is beautiful), but I’m working on two projects side by side, one in rust and one in go at the moment. The dependency trees are entirely different beasts, with most of the stuff on the go project covered by the stdlib whereas I think the rust project is over 400 despite asking for just rusqlite (sqlite), clap (cli), ratatui (tui), and tauri (gui), the last of which is by far the worst offender but even without it, it’s still close on 100 which is crazy. If there were (and maybe there are, I just haven’t found them) decently maintained alternatives to the rust crates that actually have a sane dependency approach, I’d feel much better. I’m just trying to not shai hulud my system, and the rust-web people seem to want to turn cargo into npm in that regard.
Note that many Rust libraries consist of multiple crates, which all end up in the dependency graph. This makes the number of dependencies seem higher than it actually is: the separate crates have the same maintainers and are often part of the same upstream git repo.
I agree with the general sentiment though. Rust also has a lot of crates that are stuck semi-unmaintained at some 0.x version, often with no better alternative.
Unfortunately the 0.x version has pervaded because of community cargo culting claiming that versioning is easier with 0.x than with major version numbers > 0. Personally I find that hard to believe, especially given packages like Tokio and anyhow (still at v1) make it work and there’s others that are >v1.
That is to say 0.x doesn’t necessarily mean unmaintained, it can also mean “I don’t want to have to think about how to version APIs / make guarantees about APIs). Eg reqwest is very widely used and actively maintained yet is still at v0.13.
> claiming that versioning is easier with 0.x than with major version numbers > 0
I think it's less that versioning is claimed to be easier with 0.x versions, and more that some people have got into their heads that 1.0 signals either "permanently stable" or "no new versions for several years" and they don't want to commit to that yet.
I do wish more crates would 1.0 (and then 2.0, etc).
There is good reasons to break out projects into multiple crates. It makes reusing functionality elsewhere easier. It makes it easier to reason about behavior. It makes it easier for LLMs to understand (either working within the crate or consuming as an api surface.) So you end up with projects that have multiple crates inside the same workspace and it really blows up dependency count.
The other very important reason for splitting into crates is compile times. Crates are the "compilation unit" and you often get more build paralellism with more crates.
To highlight the problem for Python: Python's standard library has getopt, optparse, and now argparse. I don't think they set out to offer 3 argument parsing libs, one of which is marked superseded, but here we are.
At least in the case of sqlite, rusqlite pulled in 5 or so in total whereas Go had a single library that was a thin wrapper around sqlite, and integrated into the stdlib interface. Many fewer deps
Edit: counts are fair, that’s still hundreds unaccounted
Package management is the bane of nearly every language/technology
Nobody has "solved" it, and I don't think that there will ever be one (never say never, though, right?)
For Go we rely on developers of libraries to adhere to the semver versioning scheme accurately, and we cannot "pin" versions (a personal bugbear of mine)
There is a couple of workarounds - using SHAs not unlike the git commit hash to provide a pseudo version, and, vendoring (which is a cache of known dependencies - which brings with it cache management problems)
I had the misfortune of having to use Python with a virtual env on the weekend - it did not end well, and reminded me why I migrated away from Python.
Look at
Perl (cpan)
Java (maven, gradle)
Ruby (gems)
Go (dep, glide, vgo, modules)
Rust (cargo)
Node (npm, yarn, etc)
OSes too
Redhat (yum, rpm, etc)
Debian (apt)
Ubuntu (snap - god why????)
It did and didn't. Nix tools for building language-specific packages almost always wrap the language build tool/package manager. This can be easy or hard, depending on how onerous the build tool is for vendoring libraries.
What Nix and build tools need to agree on is a specification or protocol for "building a software dependency tree". Like, I should be able to say 'builder = cargo' in a Nix derivation and Cargo should be able to pick up everything it needs from the build environment. Alas, there is simply far too much tied up in nixpkg's stdenv for this to be viable, so we have magic stdenv builder behavior via hooks when a build tool is included in nativeBuildInputs.
I think one of the key problems too is that a system level dependency is managed by people dedicated to ensuring the chaotic nature of the package they are responsible for conforms to the way the OS they are maintaining for has proscribed.
There's no real way to do that at a language level - we cannot have "Go has determined the package you are trying to fix has not met the versioning requirements proscribed so you cannot submit the patch to fix it"
What language dependencies do is what OSes would think of as "unofficial versioning" that is, an OS will let you install and run an unofficial version of some lib (we've all been there, right, multiple versions of some core library because one doesn't work with whatever you are trying to install), but they will not manage it at all.
That's an interesting viewpoint, but one I've noticed is less prevalent in other languages.
The c# guys at microsoft created an enormous stdlib, and the overwhelming majority of it is pretty good. The outliers being of course older stuff they've never really had time to upgrade. And they don't seem to be afraid to deprecate stuff, every major version brings a couple of minor breaking changes. But it all seems to work out just fine somehow
The stdlib isn't necessarily better, but it's always there. To use Python as an example, I tend to prefer requests to urllib2, as do most programmers. But I've absolutely been in scenarios where all I could get was the stdlib, and having urllib2 saved my ass. I think it's extremely important for the stdlib to be batteries included, even if they aren't the best versions of those batteries on the market.
Why is it worse to import a number of other packages that provide exactly the functionality you need, than to have a large standard library that provides some but not all of the functionality you need, requiring you to still use some large dependencies?
A lot of libs/packages in Go's stdlib also has this problem. They like to package everything in a very tight interface (very obvious example includes crypto/* and http), without exposing implementation detail to the end user.
Doing this of course has it's benefits, but if the feature provided by the stdlib slightly don't fit you needs, then you might have to write your own (potentially unsafe and/or less performant) one from zero.
Rust is great overall, but there's some oddities. For example their lib.rs / `mod` is very, very unintuitive, it felt overdesigned and unnecessarily complex (just see [their book]). I like what Go or Java did to their lib/package systems, it's much better that way.
I've come to hate hiding internals. Put them in a namespace which makes it clear there's no API stability guarantees, but make them available if needed.
As you note it's just pain with no gain to properly hide them. Users can't readily work around bugs or extend functionality.
LLM writing tells are getting more subtle, but they still jump off the page for me, in particular the word "genuine:"
"This is the area where Go genuinely shines, and it’s worth being precise about why"
"the lack of GC pauses is a genuine selling point"
"Humans are genuinely bad at reasoning about memory"
"There are cases where the borrow checker is genuinely too strict"
tbc I don't think the article was fully AI-generated, just AI-assisted. If so, the author did a genuinely good job of it! No one else is commenting on it, so clearly it didn't detract much from the substance. It's just weird that this is becoming increasingly common, and increasingly hard to detect.
And it’s a good contrast with ‘just fcking use Go’ article he linked.
Go article is much more human. I love that and would choose a human centered language and human centered culture over LLM-centered everything every time
I have to agree here, but I'm not sure why. I don't have any clue what makes something sound AI generated or not. I got to about here "Go is clearly working for a lot of people," -- before I became suspicious that it was AI-assisted (but also maybe I'm wrong and it's not AI-assisted, I am very bad at telling). It's more about vibes (ironically) than anything else in particular. If something "sounds" AI-assisted then I instantly lose interest even if the article itself is otherwise fine. I wish people were more ok with writing their own thoughts with how it comes to them.
Agreed. In fact, one of the things I now watch for is my mind starting to "slide off" the text, or finding myself re-reading a section multiple times. It's like the brain subconsciously recognizes a lack of substance even if we can't point to a specific tell.
While reading the article, I remember feeling that I'm reading an LLM generated sentence a few times, but in general, this specific article look like an example of acceptable LLM usage to me. I wouldn't call it "AI slop".
It is, if I may say that, _genuinely_ hard to use LLM assist and not make the text look like LLM generated. Even when I write an email in gmail and it gives its suggestions to make the text better, each one individually makes perfect sense, but when I click a few of them, the whole email now looks like AI slop, so I would normally undo the changes, going back to my imperfect hand-written non-optimized version.
I've noticed LLM writing over the past year has had an unusually high tendency to talk about surfaces and, in particular, substrates. I don't expect LLM generated text to be anything other than rich with clichés. I simply wish we would all demonstrate a better editorial hand so we weren't reading the same voice, over and over.
I think the whole post is AI generated. The author could have given a draft as input and perhaps edited the output in a few places.
Take this paragraph as example:
> Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions.
Every sentence says something. Every sentence is important and holds its weight. I would expect that kind of writing from very specialized books or papers, not from a blog post. Also, it makes the post harder (and more boring) to read.
If you have a green field, by all means write it in rust. If you have a brown field, and a functional profitable system, rewrite the parts that need rewriting in the original language, whatever that is, and carry on. Make your systems better in small measurable ways, with the language you know and a team you trust to implement it all. Anything else is a wasteful religious argument.
If anyone one comes and tells me we need to rewrite in a new language from any of those modern languages, other than you are dealing with something cannot wait for GC.
That is a signal that person is lacking purpose in their job or life.
I liked Rust before running a benchmark, but the gap between how effectively most LLMs write in Rust vs Go was still surprisingly large to me (especially in agentic harnesses where they can fix the initial environment issues). I've become a pretty big Rust evangelist after seeing that. We've had a lot of success writing batch processing tools in Rust to be called by our existing codebase, but haven't attempted a full production migration... yet.
I will say that many of the issues with Go in the article, especially re: nil handling are increasingly solved by thorough coding reviews with Codex. Better to not have the issue in the first place, sure, but these kinds of security bugs are becoming optional to developers who put in at least as much effort to review and understand code as they put into the initial design and execution.
The detailed compiler errors and strong type system makes the change -> compile -> change loop simple for agents to handle. Rust provides very strong rails it forces users on to. Codex always manages to get something to compile.
The downside is that maybe it should fail sometimes when an idiomatic approach isn’t viable… instead it will implement something stupid that compiles and meets the request.
The weakness of Rust WRT LLMs is compilation times. LLMs code faster and hence spend relatively more time waiting for compilation than humans do, so on reasonably sized projects (e.g. 100k+ lines) Rust's ~10x slower compilation starts showing up as a bottleneck. If you're writing some critical infrastructure it makes sense to pay that cost, but if you're writing some internal service that's not publicly exposed to the internet then development velocity may be a bigger concern. (I'd argue that slow compilation also influences human development velocity, but for some reason developers very rarely try to quantify this.)
I already use Rust and don't have experience with Go, so this article maybe isn't super for me.
I do have one nitpick though: Stating that data races are "caught at compile time" in Rust feels like it is overstating the case, at least a little. It sounds a bit like its implying Rust can also handle things like mutual lock starvation, or other concurrency issues. When that's simply not the case. I know "data race" is technically a formal term, with a decently narrow scope, yet I still think it could be a bit clearer about it.
Very nice write up! I am a fan of Rust and have little exposure to Go. That said, a couple of very minor points:
cargo audit is not built-in, it is 3rd party. (The comparison table near the top isn't clear about that, and the following text stating more is built-in for Rust than for Go might be confusing. I would suggest adding an asterisk to mark built-ins in that table.)
cargo watch has been in "maintenance mode" for some time. The author of that suggests cargo bacon instead.
Not sure the article is … accurate? Go has a large standard library. Rust leans on third party cargo libraries which fall into the supply chain attack and has a small standard library. Anyways, that feels immediately biased in the article. Also 11% use Rust? I don’t see that penetration in real long term products. Sure lots of tui apps these days but not things that you can make money working on.
These percentages are from the JetBrains State of Developer Ecosystem Report 2024 on the question "Which programming languages have you used in the last 12 months?"[0].
I think a better datapoint would be the "Primary Programming Languages" in the 2025 report[1] where Rust sits at 4% and Go at 8%.
perhaps the oncall is better if you write your own services, but as an SRE / ops person who has to run other people's services, rust ones just generally seem to be worse: logs that are so verbose but seem to tell you nothing, statsd seems to be the only choice for metrics, contextless errors everywhere, memory "leaks" (more like runaway memory use) that the developers swear are impossible because it's rust, overall just less mature across services written by both in house and oss teams
I was a Go engineer for years and have shipped a lot of Go. I never properly learned Rust.
Over the past year I've been using AI to write small Rust tools for myself — I barely read the code, and honestly it just works.
But for serious projects I expect to maintain long-term, I still pick Go. Today I want code I can actually own and reason about myself.
Give it a year or two and I probably won't be writing code by hand at all. Once the AI owns the code anyway, that reason disappears — and at that point Rust's guarantees win. So I suspect I'll end up leaning Rust.
> But for serious projects I expect to maintain long-term, I still pick Go.
Maintenance is a big win for Go imho - that you can go to code you wrote a year or more ago - and jump right back into it, with little-to-no re-learning curve. The syntax is not providing cover for complexity bombs, and the tools keep the workflow simple and quick.
How is it with Rust ? Does one's own old code remain maintainable ?
I've swinged between Go and Rust for my personal projects multiple times. For work, it is decided by the management so not my problem.
The biggest gripe I have with Go is the lack of *any* compile time check for mutex. Even C++ has extensions like ABSL_GUARDED_BY. For a language so proud on concurrency, it is strange not to have any guardrails.
Channels are not for everything. Plenty of mutex cases cannot be rewritten as channels, or will be very unwieldy so. In fact, every large Go project I have seen uses mutex here or there.
I do like using Rust quite a bit, but the presence of arbitrary build-time code in build.rs is very risky until we get better at implementing dev-time sandboxing.
It is also easier to make your code deterministic with Rust vs with Go, which is incredibly useful if you need to perform deterministic simulation testing + property-based testing. I recently wrote a Postgres-to-Iceberg data mirroring tool [1] in Go, but I ported it to Rust because I wanted the ability perform DST without fighting Go's runtime [2]. But if the domain is not critical that warrants DST, I would still pick Go over Rust any day.
The "when to enforce it" framing is what sticks with me. Go and Rust agree on safety, concurrency, simple deployment, but Go says "catch it in review" and Rust says "catch it before it compiles." The right answer depends entirely on how expensive a production incident is for you vs. how expensive slower iteration is.
Go has shorter and more predictable GC pauses. If a reference count drops to zero in Rust, it may take an unbounded time to free all the things it refers to (recursively if necessary).
I still think rustfmt made a mistake by going with four spaces. It's basically inferior for everything except forcing everyone to use the same indentation width, which is actually a downside, since I constantly encounter two-space indent codebases that I can't read and also can't change to four spaces because it's not tabs. Also translating spaces to tabs visually is undecidable thanks to alignment, while the inverse is not true. Ugh.
Yep, but because it's not the default, plenty of ecosystem tooling just does not properly track the two separate types of leading whitespace (indentation vs alignment) and will happily conflate every tab_width characters of alignment with an indentation level (which is grossly incorrect). I don't have an example off the top of my head because I run very far each time it happens.
Rust is great. However in an agentic world go will win. Look no further than incremental build times. This, combined with high token costs mean that for a given application it simply will cost more to to write it in Rust than Go.
This can easily be justified for many usecases, but for your vanilla crud app, do you really need Rust?
Per the article, you are getting 20-50% better more performance with Rust. Not worth it unless your team was already fluent in Rust. Now consider a scenario where your team uses AI exclusively to code, now you are spending more time and tokens waiting around to consume large rust builds. As far as I know this is an inherent property of Rust to have its safety guarantees.
I think Rust makes sense for a lot of cases, but for a small web service, overkill and unnecessary imho. If someone ported their crud app from Go to Rust I would question their priorities.
Again I am speaking more in terms of software engineering economics than anything else. Yes, I know in a perfect world Rust binaries are smaller, performance is better and code more “correct”, but the world is hardly perfect. People have to push code quickly, iterate quickly. Teams have churn, Rust, frankly is alien for many, etc.
Because the agentic world involves the generation of so much code that gets harder to review, I would think the compile-time guarantees of Rust would make it a better option.
This is true if the token budget and time are not taken into account. In practice though, waiting minutes instead of seconds per build multiplied by prompt and again by change adds up very fast.
My point is that it isn't necessarily that fast. It is relative to the amount of changes and where they were made. For a fair comparison you would also have to present the worst case incremental build time which approaches the full build time (this goes for Go too), which per your own example is nearly a minute for rust.
> For a fair comparison you would also have to present the worst case incremental build time which approaches the full build time (this goes for Go too)
The worst case that would approach a non-incremental build time would be if you were editing a leaf crate. But in almost all cases the leaf crates are 3rd-party dependencies that you would never edit directly.
A real-world worst case is probably more like ~10-20% of an non-incremental builds.
1.5s for a massive project, on a laptop,like the OP said is still barely anything in the context of agentic coding. It’s less than a single percentage point of the total time in the loop, even if the agent has to compile multiple times.
This is cope.
I do give you that rust is more verbose and thus more token heavy. However that verbosity is meaningful and the LLM would have to spend tokens thinking about the code to understand less verbose languages. So I’d consider that a wash - in some cases it hurts and in some it helps.
We don’t know how massive the project is, but in any case building and immediately building again of course will be fast. How fast is it if all files have a single line changed, for example refactoring a log message?
Not to mention we haven't even gotten to discussing tests.
Can you clarify how you're spending tokens on waiting? My understanding is that the LLM isn't actually necessarily doing anything while a build runs. The whole process end to end may take longer for sure (ignoring things like the compiler catching more errors, that's really hard to factor in) but how does that correlate to more tokens?
> The whole process end to end may take longer for sure (ignoring things like the compiler catching more errors, that's really hard to factor in) but how does that correlate to more tokens?
This. rust emits more information both in its output and the syntax itself more complicated requires more tokens.
Of course, there's plenty of bugs in Rust code still. The fact that safe Rust should be able to statically guarantee entire classes of bugs like data races are impossible is a huge deal, though. We're totally free to have different values when it comes to what matters, but compile time and a verbose toolchain are not high costs for that, to me. I personally would first consider other things like the cognitive overhead of learning to work with the borrow checker.
> As far as I know this is an inherent property of Rust to have its safety guarantees.
From what I've seen, Rust's strictness is actually a huge win for LLMs, as they get much better feedback on what's wrong with the code. Things like null checking that would be a runtime error in Go are implied by the types / evident in the syntax in Rust.
Go is more verbose, but Rust have more complex syntax which in practice require more tokens.
The big thing though is because builds are slower, you will end up waiting longer as tests are modified, rebuilt and run. This difference piles up fast.
As someone with a background of consulting in the Stockholm based gaming industry for the last decade+, I have to respectfully disagree. Nearly everyone I know is very much on the hype train. And for good reason too! The capabilities are undeniable!
The capabilities are very much deniable. They do not exist. Using LLMs to write code is either going to make you slower (as you have to review all that code), or make your software suck (because they write bad code and you're choosing to not review it). There's no actual win to be had here.
Oh yeah, definitely. There has indeed been a lot of hype overestimating the capabilities. People thinking you can one-shot big complex applications with a few paragraphs of descriptions for example. There has also been a lot of anti-hype, or whatever you call it when people seem to believe LLMs don't provide any value for software Dev, basically writing all capabilities off as pure hype.
The truth of course is somewhere in the middle.
It's difficult to tell what people mean when they say hype sometimes.
I could see migrating from C or C++ or Python to Rust, for various reasons, but for web back-end work Go is a good match. I write almost entirely in Rust, but the last time I had to do something web server side in Rust, I now wish I'd used Go.
The OP points out the wordyness of Go's error syntax. That's a good point. Rust started with the same problem, and added the "?" syntax, which just does a return with an error value on errors. Most Go error handling is exactly that, written out. Rust lacks a uniform error type. Rust has three main error systems (io::Error, thiserror, and anyhow), which is a pain when you have to pass them upward through a chain of calls.
(There are a number of things which tend to be left out of new languages and are a pain to retrofit, because there will be nearly identical but incompatible versions. Constant types. Boolean types. Error types. Multidimensional array types. Vector and matrix types of size 2, 3, and 4 with their usual operations. If those are not standardized early, programs will spend much time fussing with multiple representations of the same thing. Except for error handling, these issues do not affect web dev much, but they are a huge pain for numerical work, graphics, and modeling, where standard operations are applied to arrays of numbers.)
Go has two main advantages for web services. First, goroutines, as the OP points out. Second, libraries, which the OP doesn't mention much. Go has libraries for most of the things a web service might need, and they are the ones Google uses internally. So they've survived in very heavily used environments. Even the obscure cases are heavily used. This is not true of Rust's crates, which are less mature and often don't have formal QA support.
I love Go and used to write it heavily for anything non LLM based.
Now that we have agentic coding I just write everything in Rust and couldn’t be happier. The struggle with rust was writing it, go was made so it was easy to write for mid level engineers. Now that we have agentic coding I’m not sure Go’s value prop holds up anymore
My rust services have been nothing short of amazing from a performance and reliability perspective
For me the bottleneck now is reading/reviewing code, not writing code. As you said, AI makes it way easier to write, but do you not review the code? And isn't a verbose, cryptic language with lots of nitty gritty memory management not harder to read/review?
I'm not sold on Rust being a great language to use with AI unless the reason to use it is a lot more than just Rust being fashionable.
It's the same logic for human and for AI code: In Rust the compiler catches many bugs so you don't have to.
If the LLM gives you safe code you know there are entire classes of things you don't have to review for.
That said, I agree with you. My experience is that LLMs are great if you are highly competent in the domain in which you let them work. And it's probably easier to be competent in Go than in Rust.
Reading the code? Who has the time.
Aah, I am sure the chickens of vibe coded origin, will never come to roost.
The build time and space for rust is awful for fast iteration e.g change a thing, verify.
> Rust lacks a uniform error type
Rust has practically one error, it's the Error trait. The things you've listed are some common ways to use it, but you're entirely fine with just Box<dyn Error> (which is basically what anyhow::Error is) and similar.
Having many semantic options for error usage is functionally the same as having many error types, except worse.
They all convert seamlessly, and the enums make the branches explicit. Don't even need to check the documentation to find which errors supposedly exists like in Go with its errors.Is, errors.As, wrapping and what not.
An easy rule before you make a knowledge based choice is Thiserror for libraries, helping you create the standard library error types and Anyhow for applications, easy strings you bubble up.
Or just go with anyhow until you find a need for something else.
https://crates.io/crates/anyhow
https://crates.io/crates/thiserror
I’ve repeatedly tried using Rust and the error handling has tripped me up every time and has been ~90% of the reason for moving a project back to another language. I’m sure I’m just holding it wrong, but what I run into usually goes something like this (mind you, I have read the Rust book):
* Someone tells me to use enums for errors, in a comment like yours
* I try writing the enums by hand, implementing the error trait
* I realize that in order to use the ? operator I need to implement From on my errors (I’ve read so many comments about how awfully verbose Go errors are, so I assume I’m supposed to use ? in Rust). There are also some other traits IIRC but I’ve forgotten them.
* I realize that this is pretty tedious, manual work, so someone points me to thiserr or similar
* Now I’m debugging macro expansion errors and spending approximately the same amount of time
* I ask around and someone tells me not to bother with thiserr and to just write the boilerplate myself or else to use anyhow or boxed errors everywhere
* I try using boxed errors everywhere, which works, but now I have all of these allocations which feels like I’m doing something that will bite me later. Oh well, but now I need to annotate my errors so I can figure out what is actually happening. I guess I should use anyhow for this?
* Anyhow mostly works but this is approximately as verbose as the Go error handling that I’m told is Very Bad, and when I ask for code review most Rust people are telling me not to use anyhow because errors should be enums, at least in the API surface
I’m sure I’m doing it wrong, but as with many things in Rust, the Right Way is so rarely clear and every other Rust person gives different advice about how to solve my problem and the only thing they seem to agree on is that Rust has an easy solution and that I’m following the wrong advice. (Similarly when I had lifetime problems and half the community told me to just use clone and Rc everywhere until I had performance problems, so instead I just had different static analysis problems).
I don’t love Go’s error handling. It feels like there has to be something better than its runtime-typing. But it largely gets out of the way—creating an error is just implementing the Error method, and if you need a concrete type you use Is/As/AsType. Wrapping is fmt.Errorf. All of this is built into the stdlib and used pretty ubiquitously across the ecosystem—I don’t run into “this dependency uses a different error framework”. Error handling is marginally more verbose than with Rust if you are actually attaching context in both, and neither solves the problem of which call frame attaches the context about specific function parameters (e.g., which level of error context specifies that the function was called with path “/foo/bar.baz”). It’s terrible, but it works—feels like the least bad thing until the Rust community can arrive at some consensus and document it in The Book. Or maybe I just need to try again in the LLM era?
> “this dependency uses a different error framework”.
Common in HTTP land. The HTTP system returns a different error type than the network I/O system, but they can be sorted out.[1]
[1] https://github.com/John-Nagle/maptools/blob/main/rust/src/co...
Maybe you have reached the point where it has become more efficient to think for yourself rather than follow anyone's advice?
I know you’re trying to snark, but I’m clearly thinking for myself—that ought to be evident from the first sentence of my post. :)
How come you get macro expansion errors? Or is it because you write incorrect syntax in the enum error definitions?
The example on the docs page is quite clear:
https://docs.rs/thiserror/latest/thiserror/#example
Including all kinds of errors: Strings, tagged unions and automatically converting from std::io::Error with added context.
That one page document is the entire documentation for the thiserror crate.
It’s been a while, I don’t remember the details, but it wasn’t syntax errors.
Surely you need an alternative to Box<dyn Error> for reporting memory allocation failures?!
Anything other than panic/abort on allocation failure is outside the scope of the vast majority of programs, including anything using the standard library in Rust. I wouldn't worry about Box<dyn Error>.
A &(dyn Error + 'static) should be fine for that; you don't need any allocated/variable sized data in a memory allocation failure.
stacktraces? might also be useful to know whether or not the latest allocand was a jumbo sized allocand that caused the failure?
Do you really want that data passed back down to the caller of the allocation? From the description of the failure state you'd want to log that data instead: what's the caller of the allocation going to do if you tell it it failed with a crazy size? It already knows the size, it's the one who asked for it.
So, suppose it's a rust library -- you're locking me into whatever logging system the library author chooses? Maybe I'd like to consume the relevant data at the entry point and send it to a logging system of my choice.
A Rust library likely wouldn't be returning an opaque Box<dyn Error> to begin with. Errors are part of a library's API—it's what allows consumers to handle them—so you'd define an enum of possible errors your library could produce and return that, which would be stored on the stack.
What about the data in the error payload?
That's part of the error enum.
This enum has a known size and doesn't require any dynamic allocations.usually “stdout” is good enough, wrapper/runner routes output to logserver for collation and search. who cares about formats as long as it’s reasonably structured and searchable?
You're already writing Rust in a very different style if you're writing the type of code that gracefully handles allocation failure. It's to Rust's immense credit that this type of coding is actually fairly well-supported (unlike in Go), but you're already a bit off the beaten path for stuff like error handling.
For me the main advantage of Go over Rust is compilation speed. Then compared with Go Rust still rely on many C and C++ libraries making it problematic to cross-compile or generate reproducible builds or static binaries.
The minus side of Go is too simplistic GC. When latency spikes hit, there are little options to address them besides painful rewrite.
if you are hitting pauses due to GC issues, you should into putting appropriate data structures into a memory arena, here's a reasonable read:
https://uptrace.dev/blog/golang-memory-arena
These are all tools. Java used to have this all the time, and we (ex-java programmer) had ways around this until the JVM improved.
I've run into GC pauses, I think in many (most?) cases there is some class of bulky data that you can either move into slices of pointer-free structs (so the GC doesn't scan them) or off-heap entirely. The workload where GC is slow is also likely prone to fragmentation so whatever the language you'll have to deal with it.
Java with its copying GC deals fine with fragmentation albeit at the cost of more upfront memory. And even in Rust one can change the allocator to try to deal with fragmentation. But with Go there is simply no good options besides the rewrite.
Rust compilation speed is a matter of tooling, they could have something like OCaml or Haskell interpreters, which so far hasn't been a priority.
Or having Cranelift as default backend.
What kind of apps are you writing where GC spikes matter?
trading, networking, gaming, ai, realtime, almost anything with hard response requirements.
Isn’t it somewhat easy to remove allocations in Go? I haven’t had to “rewrite” as such, but rather lifting some allocation out of loop. Am I misunderstanding the scenario?
With backend serving many clients with widely varying performance profile of individual requests when latency spikes happen there is no particular hot loop. Just many go routines each doing reasonable thing but with a particular request pattern hitting pathological case of GC.
Removing enough allocations to avoid fragmentation can be maddenly difficult/tedious.
I agree! The line early on about this being for backend services caught my attention. I love the Rust language and use it for embedded firmware and PC applications, but still use Python for web backends, because Rust doesn't have any tool sets on the tier of Django (Or Rails). It has Flask analogs, without the robust Flask ecosystem. I have less experience with Go, but would choose it over Rust for web backends, for the same reason you highlight: The library (including framework) ecosystem. I am also not the biggest Async Rust fan for the standard reasons (The rust web ecosystem is almost fully Async-required).
Conversely, the Go community tends to actively shun frameworks, especially anything Rails-like, and will tell you to just use the standard library. Which is good advice, the standard library really does have everything you need. But it's also roughly on a par with what's available in Rust (though as someone said above, the Go stdlib routines have been heavily, massively, tested in production by now, and are fully mature and load-bearing).
Interesting! Are Go backend building custom auth, admin, DB ORM/migrations/auto migrations, templates, email, dev server etc for each project? Or each person and org has their own toolkit they use?
We tend not to use ORMs, because they're evil.
There are various libraries people use for auth, etc. But rolling your own isn't hard - Go has (e.g.) bcrypt in the standard library, so most of the heavy lifting is already done, you can write a solid auth implementation in <50 lines of code using that.
Generally Go prefers libraries to frameworks. Wrap the hard bits up into a library that can then be used widely in any implementation, rather than rolling it into a one-size-fits-all implementation that doesn't really suit anyone properly.
please don’t generalize. there is no “we” ..
“we” are all different and i can tell you from experience that there are also many people and teams who use go and prefer ORMs and frameworks and do not build everything from scratch …
true, but there does tend to be a consensus (or has been) in the Go community around a lot of this stuff.
For me going from JVM and CLR ecosystem of programming languages into Go for backend development is a downgrade.
The language design makes sense in the context of Oberon (1987), and Limbo (1995).
Now when there are so many options finally building on top of Standard ML, and Lisp heritage, having to settle with Go feels like a downgrade.
I code since 1986, if I wanted if boilerplate error handling, or having cost as the only mechanism to declare constant values, there have been plenty of options.
For backend web dev, there are advantages. I really like Axum's use of typing:
With a route like: …the "dataset_id" path variable is parsed straight into the dataset_id arg, and a query string "verbose" is parsed into a boolean. Super convenient compared to Go, and you type validation along with it.Many other things to like: The absence of context.Context, the fact that handlers can just return the response data, etc.
What I don't like: Async.
go is slightly more verbose (surprise) but you can achieve the same thing using struct binding in gin:
This is actually a great example - what happens in that Rust version when the input parsing fails? Go makes it explicit.I'm not sure if that's a great example. What kind of errors could ShouldBindQuery return?
I would assume Axum returns a bad request error for you when query parsing fails, but if you do want more control over how the error is handled, you can change the parameter type to Result<Query<bool>, QueryRejection>, and the type system itself documents precisely what errors you can match against.[0]
[0]: https://docs.rs/axum/latest/axum/extract/rejection/enum.Quer...
The Go standard library has learned to interpret path variables as well:
https://go.dev/blog/routing-enhancements
You can not use async right? Maybe not with axum but I imagine there are fully blocking frameworks for rust.
Rust does not have three error systems. It has one: the Error trait. io::Error is one of many that implement it (nothing special about it). Errors defined via thiserror also implement it.
“Anyhow” just allows you to conveniently say “some Error” if you don’t care to write out an API contract specifying types of errors your function might spit out.
He's not making that up; in practice, you're going to run into and need to make mental space for the idiosyncrasies of multiple error frameworks.
Not sure what you mean by that. If you're consuming the API of a crate that has functions that return errors, you're not really dealing with a "framework", you're just dealing with whatever the `E` is in the `Result<T, E>`. If that `E` doesn't implement std::error::Error, I'd consider that a deficiency (even a bug) for that crate. (Yes, I know some crates want to support use in `no_std` environments; that's what features are for.)
If I care about the specific variants of error that a function can return, so I can do different things depending on what kind of error occurred, I'll read the docs and match. That's not really a "framework" thing; that's just a basic thing that anyone has to do in any language in order to consume an API. If I need to propagate the error, I'll do so (either directly, or by wrapping it in a variant of my own error type). I don't see how any of this is "framework"-y.
A crate's decision to use thiserror (or not) does not matter to me. If a crate exposes `anyhow::Error`, that's a lazy choice and bad API design, but still "works" and I generally don't need to care about it.
Or is there something else you meant when you said "error frameworks"?
I guess you might have to if you need to use a library someone's written that doesn't implement the standard.
Writing primarily applications, I couldn't tell you what error handling frameworks my dependencies are using: I literally don't know, and haven't needed to know in order to display, fail, or succeed.
EDIT to add: I use anyhow for this, so I should also add "add context to an error when I fall" to the list of things I do.
What's the standard? I'm not being snarky; I'm going down the thought process of how this would work in practice.
I am on team Io Error [on std rust]", somewhat arbitrarily. If I call a lib that is on Team Anyhow, or Team Custom Error Enum, I will have to do some (Straightfoward, but a little clumsy) conversions if I want ? to work. This is complicated by being able to impl From<ErrorType1> for ErrorType2 only in one direction if you don't control the other crate. (due to the orphan rule)
By standard I meant an error type that implements std::error::Error.
EDIT: Which I assume all my dependencies have done, given that anyhow is able to consume all of them.
I specifically called out writing applications as my use case: my only objection to tptacek's note is the somewhat universal "in practice". The burden for designing errors for a library that others will use is higher, but that's far from the default/universal experience.
Many more people are going to consume libraries & not produce any of their own, and I think my experience is representative there.
There is no team io::Error. There is only one standard: https://doc.rust-lang.org/core/error/trait.Error.html
Not rust specific, and most certainly not a criticism of you - but I hate when people call a lib that errors, then just bubble that error up.
I mean the error is supposed to be tailored to the audience - I guess what you are saying is that you handle the error by saying "I called foo with X, Y, Z, and got this error back" in the logs - which your caller then also does - producing a log message of
ERROR: I called Foo with X Y and Z and got error: Die MF die
followed by
ERROR: I called Bar with X Y Z and a and got error: ERROR: I called Foo with X Y and Z and got error: Die MF die mf (still fool)
And so on and so forth.
If the counter is - don't log, that's fine, but you have to know where in the call graph that error state was reported to the logs
I have tried to figure out some kind of unification between "collecting error state in a function", "logging error state", and "return error state to a parent".
I haven't found any satisfying solution to it all; collecting information for logging vs information that a caller would want... I've been meaning to investigate tracing_error to see if it brings it all together.
Regardless of language - if you find a good, clear answer - blog the hang out of it - I for one have been searching for the right way to manage this, and it's not (yet) clearer - other than what I've said so far
You’re supposed to bubble errors up to the level that can appropriately deal with them? You don’t need to log them each step of the way.
Yeah - but that's the same as my final point - you have to know who is supposed to manage the error/log - all the way up (and down) the call graph
edit: I've just finished debugging a multi system chain - FE -> SNS -> SQS -> Lambda -> DynamoDB -> Lambda -> Webhook -> My poor code
My code has multiple layers - and I was trying to find where in the very long chain of calls the data was being mangled
It turned out that there was an unlogged error, which was mismanaged by a caller - there's no shade here - the caller was handling the error how it was designed to, but by not logging that there was an error - it took a minute to understand.
I was a big fan of go for a while. Though now that I have programmed more swift and rust recently, having a compiler that doesn’t protect against null pointer deferences or provide concurrency safety guarantees feels a little prehistoric.
Though go certainly did a much better job than rust on the standard library front.
Standard library is something you have to maintain for all eternity, with identical API. It had been argued that some concurrency primitives like channels would have been better outside of std (for rust, to be clear). Once dependency management is solved, a small std is beneficial.
> you have to maintain for all eternity, with identical API
People always tout this as a huge reason for not wanting a too big std in Rust (or "too useful" either), but IMHO that's just talking about reaching theoretical optimals, while leaving the community for years without good guidance via providing a opinionated practical and pragmatic way of doing things. Which I find to be a very unhelpful stance for a tool such as a programming language.
If a design of some std package didn't pass the test of time, and a new iteration would be beneficial, the language can leave its original API version right there, and evolve with a v2, with an improved and better thought out API after learning from the mistakes of v1.
Prime example: "hey we found that math/rand had some flaws, so here is math/rand/v2". A practical solution, and zero dramas as a result of having rand be part of std.
Perhaps it would help if stdlibs were be versioned, with the chosen version declared in the project file. For existing languages, a lack of version would simply indicate the original stdlib, meaning nothing should break.
I definitely don't think stdlibs should be changed often, but it seems fairly damaging to a language when things may be added to a stdlib but never removed, no matter how broken or misconceived (see C++).
Rust is a great language, but the poor stdlib + overreliance on crates + explosion of unvetted transient dependencies makes it a hard sell for a lot of projects.
I use go because of the large and useful stdlib. I rarely have to reach for an external library, and even then I only consider libraries that are very popular. If a library isn’t available, I’ll just write my own, using the stdlib. I recently used the awesome crypto library to implement an envelope encryption system, I didn’t need anything outside of the stdlib or Google’s x library (x is effectively the experimental stdlib).
Having too much external code, like npm or rust crates, seems like a nightmare for me.
Doesn't Rust already have that solved via editions? If anything, that's the language that's especially well positioned here.
The funny thing is: All Rust source code looks like an assembly syntax error.
If you spend an hour skimming the Rust Book it's not really that bad
It's not that bad, but when compared to C or Go, it's not something that I would like to type by hand. At least Java has IDEs which reduce the amount of verbosity you need to type. I know you get safety, but the verbosity and cargo is is a con in my opinion.
thiserror and anyhow are just std::error with extra steps. Note that io::error is just a specific std::error.
The entire point in Rust is that you wrap Error impls with other Error impls, or translate one impl into another using a match. I've found this is far more flexible and verifiable than most other languages, because if you craft your error types with enough rigor, you can basically have a complete semantic backtrace without the overhead of a real backtrace.
I use thiserror a lot to help with my impls. Notably, all it does is impl Display and Error. It's not a specific other paradigm because it basically compiles out, it's just a macro.
Anyhow is perhaps the closest one to another paradigm because it allows you to discard typed information in favor of just the string messages, but it still integrates well with Errors (and is one).
thiserror and anyhow are std::error::Error with fewer steps.
I find Elixir's memory and threading models much more compelling than Go's for web services. There are many great libraries for Elixir as well, but if you need something else, Elixir makes rolling your own libraries very easy. I'd recommend giving Elixir a try, if you haven't already.
Or gleam if you don’t fancy elixir.
Praising go for how it handles errors, when it's even worse than C where the compiler at least warns you if you're ignoring return values of calls. That's a new one.
Linters are available to catch you before you compile - with Go
Generally speaking there has to be a mechanism for optional handling of return values, in Go you can ignore everything (ew), you can use placeholders `_`, or you can explicitly handle things - my preference.
If you say "Well in C you have to handle the returns - I am not across C enough to comment, but I will ask you - Does C actually force you, or does it allow you to say "ok I will put some variables in to catch the returns, but I will never actually use those variables" - because that's very much the same as Go with the placeholder approach
edit: I am told the following is possible in C
trySomething(); // Assumes that the author of trySomething has not annotated the function as a `nodiscard`
(void)trySomething(); // Casts the return(s) to void, telling the compiler to ignore the non-handling
int dummy = trySomething(); // assign to a variable that's never used again
I welcome correction
C, as a language, cannot bother less about you using or not using the return values, checking them, discarding them, or using them to index an array without any bounds checking. Various linters and compilers may have their opinions, expressed as warnings, but at the end of the day it's completely up to you as a developer.
Yeah - I assumed so - which makes the GP post... bizarre
This is a weird document that is simultaneously trying to serve as a migration guide and an advocacy document for Rust.
Ultimately, if you have to ask, the Rust vs. Go consideration boils down almost completely to "do you want a managed runtime or not". A generation of Rust programmers has convinced itself that "managed runtime" is bad, that not having one is an important feature. But that's obviously false: there are more programming domains where you want a managed runtime than ones where you don't.
That's not an argument for defaulting to Go in all those cases! There are plenty of subjective reasons to prefer Rust. I miss `match` when I write Go (I do not miss tokio and async Rust, though). They're both perfectly legitimate choices in virtually any case where you don't have to distort the problem space to fit them in (ie: trying to write a Go LKM would be a weird move).
The Rust vs. Go slapfight is a weird and cringe backwater of our field. Huge portions of the industry are happily building entire systems in Python or Node, and smirking at the weirdos arguing over which statically typed compiled language to use. Python vs. (Rust|Go) is a real question. Rust vs. Go isn't.
Exactly. 95% of programmers are application programmers - they ship software used by regular users. I think it's insane to use a non-GC language for most of those cases. Manual memory management is mentally taxing and it's easy to make catastrophic mistakes. The marginal benefit from it is just not worth it unless you're making games or a trading system.
5% who write tools or other "infra" layer for the other 95% to work on top of maybe need that level of control over memory. It doesn't make any sense to me to sign up for that complexity unless you really really need it.
Maybe I'm misunderstanding something but non-GC language doesn't mean you have to do memory management manually? I mean, for example, in Rust (or modern C++), it's basically automatic. There is no mental tax or catastrophic mistakes as far as I know.
The use of LLMs has caused Rust usage to explode.
If youre not writing the code yourself and vibing away which I think most people generally are despite the disdain around here then why would you not choose the "more performant language" (I know that isnt necessarily reality but it is a common perception).
Go's managed runtime is less valuable when the LLM is perfectly happy to slap a bunch of stuff together for you to and approximate it and doesn't complain at all when writing async rust despite some of the rough edges.
Correction: The use of LLMs has caused every major language usage to explode.
And as mentioned in other comments, Rust slow compilation can be detrimental to LLMs + fast iteration speed. And it's not just speed, Tauri takes 20GB of disk space to compile. It's bonkers. This is npm/js ecosystem all over again but slower.
Another reason to pick Go if you're leaning on LLMs is the standard library. Often you can do more work with fewer dependencies.
I'd rather leverage world class engineers paid by Google to maintain dependencies for me than try my luck with half a dozen of 0.x crates. Plus stdlib APIs can (and are) versioned just like third party dependencies.
Fully agree with this. We use Rust in an enterprise setting for building web app backends and the experience is painful. A lot of crates just seem like someones side project. Too many ways to do things leads to bike shedding in PRs. Compile times are atrocious and can take like 30 mins to build.
Honestly using Go would have got us to the same point much quicker, with code that is much easier to review.
I like vibe coding but I am sceptical that a vibe coded runtime in Rust would be as awesome as the Go runtime which is written with deep expertise of Unix software and threading and many low level details that are subtle and do depend on global properties of the code to work flawlessly. It makes sense you can crank out Rust with an LLM if you know what you are doing, but if you want a GC type thing or preemptive scheduling across an N by M threading model, then you are competing against some very good code.
> the Go runtime which is written with deep expertise of Unix software
Go has no mmap(), import a 3rd party dependency for that and you'll get a segfault the very second you do a mistake.
Python has an mmap module which will catch many memory errors and present them as exception rather than causing a CVE.
I agree that agents make Rust a lot more tenable for less "kernel-and-browser"-demanding tasks than it was 4 years ago, but I do not agree that they eliminate the "managed vs. unmanaged runtime" question, and to the extent they influence any of this decisionmaking at all, you have to accept the notion of not reading the code. If you're reading it, it matters that Rust makes you do bookkeeping that managed runtimes avoid.
> The use of LLMs has caused Rust usage to explode.
Rust had a "vibey" community long before vibecoding. In particular, it's long been fairly non-serious about yolo importing a bunch of crates to solve things (since the standard lib is small) which is kinda the same problem as having all those things just vibecoded. Either way, most projects weren't reading all of that other code!
> the Rust vs. Go consideration boils down almost completely to "do you want a managed runtime or not".
That's not really something I care much about. My beefs with Go are 90% about the syntax of the language itself, and it's weak (compared to Rust) type system.
When it comes to a managed runtime, for most tasks, I generally don't care if my language has one or not. For some tasks I do, but there are not many of those tasks, and so this question is mostly irrelevant to me when deciding Go vs. Rust.
I don't really get where you're seeing that the predominant Go vs. Rust debate is about the runtime. IME it's the subjective stuff about the languages themselves, and their ecosystems and communities.
> The Rust vs. Go slapfight is a weird and cringe backwater of our field.
::shrug:: I dunno, I mostly stay out of it and just use Rust, and I'm happy and avoid the drama. I've written a little Go here and there, didn't really like it, and moved on.
That's totally fine. I don't get why people moralize this stuff. Both of these languages are rounding errors compared to the dynamic languages.
I think people do this for every language. It becomes a part of their identity, and then they have to defend it. I used to do that too, long ago, but I don't have the time or energy for it for the most part, and find it boring, so that $LANG-user-as-identity bit of my has fallen by the wayside.
I don't think it's about adoption levels; sure Go and Rust are tiny compared to JS/python/etc. It's emotional, not about who has the most users or who can even plausibly get there.
> Ultimately, if you have to ask, the Rust vs. Go consideration boils down almost completely to "do you want a managed runtime or not".
You don't need a garbage collector which is perhaps half of the Go Runtime when you're using Rust.
You can also bolt on a few crates and get ~95% of what you'd get from Go's runtime.
Go has the best runtime in the world. I'll give it that.
But this is not the only reason...
You obviously don't need a GC when you're using Rust, because Rust doesn't plausibly have one.
Right, so you don't need a large portion of Go's runtime benefits, because you have a far better version of it already, zero cost abstractions and TRUE memory safety, not pretend memory safety behind a -race detector with zero compiler guarantees...
I don't know who you're speaking to, but it isn't me; I certainly didn't ask for the standard-issue Rust langwar pitch.
I think I'd be ok with node via purescript? But in general I think rust and go people should join forces against the evils of dynamic typing. Isn't type hinting finally considered best practice now? I think that is effectively an admission that it was a defect. And even with good ginting it is still worse than inference. Inference can let plenty of code go untouched on type changes, while still protecting against unindended type changes.
Us Node folks adapted typescript because we wanted static compiled types.
I wish TS had more of a runtime. The only thing I'm jealous of with regards to python is how seamlessly you can do JSON schema enforcement on HTTP endpoints. The Zod hoops are a constant source of irritation that only exists because the TS team is dogmatic.
I think Typescript is a perfectly cromulent language. I don't know it well but would seriously consider it for any problem that had a shape that admitted a dynamic language. There's a lot to be said for using dynamic languages, too!
Every non-runtime language is dynamic after being compiled to x64 machine code!
It is illusions and lies all the way down the instant the compiler finishes its job.
Check out Perry the TypeScript compiler to native code
This is probably going to sound generic / repetitive, but my biggest complaint about Rust is the package management situation, which is entirely the result of the developer mindset. I love the ergonomics on the rust side (the functional approach to data types is beautiful), but I’m working on two projects side by side, one in rust and one in go at the moment. The dependency trees are entirely different beasts, with most of the stuff on the go project covered by the stdlib whereas I think the rust project is over 400 despite asking for just rusqlite (sqlite), clap (cli), ratatui (tui), and tauri (gui), the last of which is by far the worst offender but even without it, it’s still close on 100 which is crazy. If there were (and maybe there are, I just haven’t found them) decently maintained alternatives to the rust crates that actually have a sane dependency approach, I’d feel much better. I’m just trying to not shai hulud my system, and the rust-web people seem to want to turn cargo into npm in that regard.
Note that many Rust libraries consist of multiple crates, which all end up in the dependency graph. This makes the number of dependencies seem higher than it actually is: the separate crates have the same maintainers and are often part of the same upstream git repo.
I agree with the general sentiment though. Rust also has a lot of crates that are stuck semi-unmaintained at some 0.x version, often with no better alternative.
Unfortunately the 0.x version has pervaded because of community cargo culting claiming that versioning is easier with 0.x than with major version numbers > 0. Personally I find that hard to believe, especially given packages like Tokio and anyhow (still at v1) make it work and there’s others that are >v1.
That is to say 0.x doesn’t necessarily mean unmaintained, it can also mean “I don’t want to have to think about how to version APIs / make guarantees about APIs). Eg reqwest is very widely used and actively maintained yet is still at v0.13.
> claiming that versioning is easier with 0.x than with major version numbers > 0
I think it's less that versioning is claimed to be easier with 0.x versions, and more that some people have got into their heads that 1.0 signals either "permanently stable" or "no new versions for several years" and they don't want to commit to that yet.
I do wish more crates would 1.0 (and then 2.0, etc).
There is good reasons to break out projects into multiple crates. It makes reusing functionality elsewhere easier. It makes it easier to reason about behavior. It makes it easier for LLMs to understand (either working within the crate or consuming as an api surface.) So you end up with projects that have multiple crates inside the same workspace and it really blows up dependency count.
The other very important reason for splitting into crates is compile times. Crates are the "compilation unit" and you often get more build paralellism with more crates.
> rusqlite (sqlite), clap (cli), ratatui (tui), and tauri (gui)
Does any language, except like Java, exist with a standard library comprising matching that?
Also, keep in mind that Tauri itself is 14 crates, where each one shows up in your build tree.
https://github.com/tauri-apps/tauri/blob/dev/Cargo.toml
And Ratatui is 6:
https://github.com/ratatui/ratatui/blob/main/Cargo.toml
Python has sqlite3[0], curses (tui) [1], and tkinter[2] in the stdlib.
[0] https://docs.python.org/3/library/sqlite3.html
[1] https://docs.python.org/3/library/curses.html
[2] https://docs.python.org/3/library/tkinter.html
Right. The famous stdlib where once good libraries go to die so you instead depend on the latest community replacement choice.
Also argparse for Clap:
https://docs.python.org/3/library/argparse.html
To highlight the problem for Python: Python's standard library has getopt, optparse, and now argparse. I don't think they set out to offer 3 argument parsing libs, one of which is marked superseded, but here we are.
At least in the case of sqlite, rusqlite pulled in 5 or so in total whereas Go had a single library that was a thin wrapper around sqlite, and integrated into the stdlib interface. Many fewer deps
Edit: counts are fair, that’s still hundreds unaccounted
Package management is the bane of nearly every language/technology
Nobody has "solved" it, and I don't think that there will ever be one (never say never, though, right?)
For Go we rely on developers of libraries to adhere to the semver versioning scheme accurately, and we cannot "pin" versions (a personal bugbear of mine)
There is a couple of workarounds - using SHAs not unlike the git commit hash to provide a pseudo version, and, vendoring (which is a cache of known dependencies - which brings with it cache management problems)
I had the misfortune of having to use Python with a virtual env on the weekend - it did not end well, and reminded me why I migrated away from Python.
Look at Perl (cpan) Java (maven, gradle) Ruby (gems) Go (dep, glide, vgo, modules) Rust (cargo) Node (npm, yarn, etc)
OSes too Redhat (yum, rpm, etc) Debian (apt) Ubuntu (snap - god why????)
And so on
Nix solved it. Languages could choose to adopt Nix as their packaging system.
It did and didn't. Nix tools for building language-specific packages almost always wrap the language build tool/package manager. This can be easy or hard, depending on how onerous the build tool is for vendoring libraries.
What Nix and build tools need to agree on is a specification or protocol for "building a software dependency tree". Like, I should be able to say 'builder = cargo' in a Nix derivation and Cargo should be able to pick up everything it needs from the build environment. Alas, there is simply far too much tied up in nixpkg's stdenv for this to be viable, so we have magic stdenv builder behavior via hooks when a build tool is included in nativeBuildInputs.
Thanks for writing this, I learned something
I think one of the key problems too is that a system level dependency is managed by people dedicated to ensuring the chaotic nature of the package they are responsible for conforms to the way the OS they are maintaining for has proscribed.
There's no real way to do that at a language level - we cannot have "Go has determined the package you are trying to fix has not met the versioning requirements proscribed so you cannot submit the patch to fix it"
What language dependencies do is what OSes would think of as "unofficial versioning" that is, an OS will let you install and run an unofficial version of some lib (we've all been there, right, multiple versions of some core library because one doesn't work with whatever you are trying to install), but they will not manage it at all.
In theory, but not in practice
The stdlib is the place where good ideas go to die.
And then you have httplib3 followed by httplib4.
In other words: I highly prefer the Rust approach.
It doesn't matter a lot whether I rely on the stdlib or another dependency to me.
It's a dependency after all.
People think just because it's the stdlib it's somehow better quality or better maintained, but these are orthogonal concepts.
In the end it depends solely on resources.
Sure, the stdlib may get more of these, but it may also grow fat and unmaintainable...
I’m not arguing on quality of the library, I’m arguing on not getting pwned by the sheer number of transitive dependencies
That's an interesting viewpoint, but one I've noticed is less prevalent in other languages.
The c# guys at microsoft created an enormous stdlib, and the overwhelming majority of it is pretty good. The outliers being of course older stuff they've never really had time to upgrade. And they don't seem to be afraid to deprecate stuff, every major version brings a couple of minor breaking changes. But it all seems to work out just fine somehow
The stdlib isn't necessarily better, but it's always there. To use Python as an example, I tend to prefer requests to urllib2, as do most programmers. But I've absolutely been in scenarios where all I could get was the stdlib, and having urllib2 saved my ass. I think it's extremely important for the stdlib to be batteries included, even if they aren't the best versions of those batteries on the market.
Why is it worse to import a number of other packages that provide exactly the functionality you need, than to have a large standard library that provides some but not all of the functionality you need, requiring you to still use some large dependencies?
For example, security. See all the supply chain attacks from the past couple of years.
Interesting. I'm not very familiar with Go. What is the equivalent for Tauri in Go's stdlib?
Would it make sense to continue using Go for the frontend and doing only the backend in Rust for your user case?
wails, there's wails3-alpha which some people said is even better than tauri
Thanks. Is wails a Go stdlib component, as GP implied or is it third party?
tauri isn't stdlib and neither is wails
> It confuses easiness with simplicity
A lot of libs/packages in Go's stdlib also has this problem. They like to package everything in a very tight interface (very obvious example includes crypto/* and http), without exposing implementation detail to the end user.
Doing this of course has it's benefits, but if the feature provided by the stdlib slightly don't fit you needs, then you might have to write your own (potentially unsafe and/or less performant) one from zero.
Rust is great overall, but there's some oddities. For example their lib.rs / `mod` is very, very unintuitive, it felt overdesigned and unnecessarily complex (just see [their book]). I like what Go or Java did to their lib/package systems, it's much better that way.
[their book]: https://doc.rust-lang.org/stable/book/ch07-05-separating-mod...
I've come to hate hiding internals. Put them in a namespace which makes it clear there's no API stability guarantees, but make them available if needed.
As you note it's just pain with no gain to properly hide them. Users can't readily work around bugs or extend functionality.
LLM writing tells are getting more subtle, but they still jump off the page for me, in particular the word "genuine:"
tbc I don't think the article was fully AI-generated, just AI-assisted. If so, the author did a genuinely good job of it! No one else is commenting on it, so clearly it didn't detract much from the substance. It's just weird that this is becoming increasingly common, and increasingly hard to detect.And many others. I felt it too
And it’s a good contrast with ‘just fcking use Go’ article he linked.
Go article is much more human. I love that and would choose a human centered language and human centered culture over LLM-centered everything every time
I guess I am just old
And tables with comparisons! Nobody makes a table by hand if it’s not packed with value. Tables in this article are not
This is completely off topic now but, "it's worth being precise about ..." is a much stronger AI-ism than the usage of the word genuine.
I have to agree here, but I'm not sure why. I don't have any clue what makes something sound AI generated or not. I got to about here "Go is clearly working for a lot of people," -- before I became suspicious that it was AI-assisted (but also maybe I'm wrong and it's not AI-assisted, I am very bad at telling). It's more about vibes (ironically) than anything else in particular. If something "sounds" AI-assisted then I instantly lose interest even if the article itself is otherwise fine. I wish people were more ok with writing their own thoughts with how it comes to them.
Agreed. In fact, one of the things I now watch for is my mind starting to "slide off" the text, or finding myself re-reading a section multiple times. It's like the brain subconsciously recognizes a lack of substance even if we can't point to a specific tell.
While reading the article, I remember feeling that I'm reading an LLM generated sentence a few times, but in general, this specific article look like an example of acceptable LLM usage to me. I wouldn't call it "AI slop".
It is, if I may say that, _genuinely_ hard to use LLM assist and not make the text look like LLM generated. Even when I write an email in gmail and it gives its suggestions to make the text better, each one individually makes perfect sense, but when I click a few of them, the whole email now looks like AI slop, so I would normally undo the changes, going back to my imperfect hand-written non-optimized version.
I've noticed LLM writing over the past year has had an unusually high tendency to talk about surfaces and, in particular, substrates. I don't expect LLM generated text to be anything other than rich with clichés. I simply wish we would all demonstrate a better editorial hand so we weren't reading the same voice, over and over.
I think the whole post is AI generated. The author could have given a draft as input and perhaps edited the output in a few places.
Take this paragraph as example:
> Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions.
Every sentence says something. Every sentence is important and holds its weight. I would expect that kind of writing from very specialized books or papers, not from a blog post. Also, it makes the post harder (and more boring) to read.
If you have a green field, by all means write it in rust. If you have a brown field, and a functional profitable system, rewrite the parts that need rewriting in the original language, whatever that is, and carry on. Make your systems better in small measurable ways, with the language you know and a team you trust to implement it all. Anything else is a wasteful religious argument.
I don't see any reasons to use Rust when your team successfully shipped and is confortable with C#/Java/Go ect ...
If anyone one comes and tells me we need to rewrite in a new language from any of those modern languages, other than you are dealing with something cannot wait for GC.
That is a signal that person is lacking purpose in their job or life.
I liked Rust before running a benchmark, but the gap between how effectively most LLMs write in Rust vs Go was still surprisingly large to me (especially in agentic harnesses where they can fix the initial environment issues). I've become a pretty big Rust evangelist after seeing that. We've had a lot of success writing batch processing tools in Rust to be called by our existing codebase, but haven't attempted a full production migration... yet.
I will say that many of the issues with Go in the article, especially re: nil handling are increasingly solved by thorough coding reviews with Codex. Better to not have the issue in the first place, sure, but these kinds of security bugs are becoming optional to developers who put in at least as much effort to review and understand code as they put into the initial design and execution.
Language data at https://gertlabs.com/rankings?mode=agentic_coding
The detailed compiler errors and strong type system makes the change -> compile -> change loop simple for agents to handle. Rust provides very strong rails it forces users on to. Codex always manages to get something to compile.
The downside is that maybe it should fail sometimes when an idiomatic approach isn’t viable… instead it will implement something stupid that compiles and meets the request.
The weakness of Rust WRT LLMs is compilation times. LLMs code faster and hence spend relatively more time waiting for compilation than humans do, so on reasonably sized projects (e.g. 100k+ lines) Rust's ~10x slower compilation starts showing up as a bottleneck. If you're writing some critical infrastructure it makes sense to pay that cost, but if you're writing some internal service that's not publicly exposed to the internet then development velocity may be a bigger concern. (I'd argue that slow compilation also influences human development velocity, but for some reason developers very rarely try to quantify this.)
10x slower is like an extra second, if that, for compilation times for the sizes of changes an agent like codex makes.
I already use Rust and don't have experience with Go, so this article maybe isn't super for me.
I do have one nitpick though: Stating that data races are "caught at compile time" in Rust feels like it is overstating the case, at least a little. It sounds a bit like its implying Rust can also handle things like mutual lock starvation, or other concurrency issues. When that's simply not the case. I know "data race" is technically a formal term, with a decently narrow scope, yet I still think it could be a bit clearer about it.
If verbosity is a main stickler, this is coming to golang 1.28 which will cut it down drastically:
https://github.com/golang/go/issues/12854#issue-110104883
That actually looks great. Thanks a lot for the link.
"services that your organization relies on, that have high uptime requirements, that are critical to your business"
Kind of funny when your Rust service runs on Kubernetes.
Very nice write up! I am a fan of Rust and have little exposure to Go. That said, a couple of very minor points:
cargo audit is not built-in, it is 3rd party. (The comparison table near the top isn't clear about that, and the following text stating more is built-in for Rust than for Go might be confusing. I would suggest adding an asterisk to mark built-ins in that table.)
cargo watch has been in "maintenance mode" for some time. The author of that suggests cargo bacon instead.
Not sure the article is … accurate? Go has a large standard library. Rust leans on third party cargo libraries which fall into the supply chain attack and has a small standard library. Anyways, that feels immediately biased in the article. Also 11% use Rust? I don’t see that penetration in real long term products. Sure lots of tui apps these days but not things that you can make money working on.
> Also 11% use Rust?
These percentages are from the JetBrains State of Developer Ecosystem Report 2024 on the question "Which programming languages have you used in the last 12 months?"[0].
I think a better datapoint would be the "Primary Programming Languages" in the 2025 report[1] where Rust sits at 4% and Go at 8%.
[0]: https://www.jetbrains.com/lp/devecosystem-2024/#KeDHWJ
[1]: https://devecosystem-2025.jetbrains.com/tools-and-trends
perhaps the oncall is better if you write your own services, but as an SRE / ops person who has to run other people's services, rust ones just generally seem to be worse: logs that are so verbose but seem to tell you nothing, statsd seems to be the only choice for metrics, contextless errors everywhere, memory "leaks" (more like runaway memory use) that the developers swear are impossible because it's rust, overall just less mature across services written by both in house and oss teams
I was a Go engineer for years and have shipped a lot of Go. I never properly learned Rust.
Over the past year I've been using AI to write small Rust tools for myself — I barely read the code, and honestly it just works.
But for serious projects I expect to maintain long-term, I still pick Go. Today I want code I can actually own and reason about myself.
Give it a year or two and I probably won't be writing code by hand at all. Once the AI owns the code anyway, that reason disappears — and at that point Rust's guarantees win. So I suspect I'll end up leaning Rust.
> But for serious projects I expect to maintain long-term, I still pick Go.
Maintenance is a big win for Go imho - that you can go to code you wrote a year or more ago - and jump right back into it, with little-to-no re-learning curve. The syntax is not providing cover for complexity bombs, and the tools keep the workflow simple and quick.
How is it with Rust ? Does one's own old code remain maintainable ?
I've swinged between Go and Rust for my personal projects multiple times. For work, it is decided by the management so not my problem.
The biggest gripe I have with Go is the lack of *any* compile time check for mutex. Even C++ has extensions like ABSL_GUARDED_BY. For a language so proud on concurrency, it is strange not to have any guardrails.
The guardrails are channels.
If you have a mutex on a structure, linters such as are packaged into Goland will catch oversights quite effectively.
If you are using fancier concurrency structures, you should consider channels instead.
Channels are not for everything. Plenty of mutex cases cannot be rewritten as channels, or will be very unwieldy so. In fact, every large Go project I have seen uses mutex here or there.
I do like using Rust quite a bit, but the presence of arbitrary build-time code in build.rs is very risky until we get better at implementing dev-time sandboxing.
It is also easier to make your code deterministic with Rust vs with Go, which is incredibly useful if you need to perform deterministic simulation testing + property-based testing. I recently wrote a Postgres-to-Iceberg data mirroring tool [1] in Go, but I ported it to Rust because I wanted the ability perform DST without fighting Go's runtime [2]. But if the domain is not critical that warrants DST, I would still pick Go over Rust any day.
[1] https://github.com/polynya-dev/pg2iceberg
[2] https://www.polarsignals.com/blog/posts/2024/05/28/mostly-ds...
Read migrating from one hype to another, developers never learn or change, do they??
It feels like yesterday when every single project was moving to Go just because it was the new hype, that was until Rust was born.
We are already seeing projects dumping migration to Rust because the grass is not always greener on the other side.
We will be seeing this again, "Migrating from Rust to XYZ"
The "when to enforce it" framing is what sticks with me. Go and Rust agree on safety, concurrency, simple deployment, but Go says "catch it in review" and Rust says "catch it before it compiles." The right answer depends entirely on how expensive a production incident is for you vs. how expensive slower iteration is.
And TLA+ and Lean say "catch it before you write any code".
> but Go says "catch it in review"
So, in production?
It's AI slop, it makes no sense
Go has shorter and more predictable GC pauses. If a reference count drops to zero in Rust, it may take an unbounded time to free all the things it refers to (recursively if necessary).
I still prefer having deterministic control over when the free occurs.
For example, I can transmit the response to the client and then free the memory afterwards so they're not kept waiting.
I still think rustfmt made a mistake by going with four spaces. It's basically inferior for everything except forcing everyone to use the same indentation width, which is actually a downside, since I constantly encounter two-space indent codebases that I can't read and also can't change to four spaces because it's not tabs. Also translating spaces to tabs visually is undecidable thanks to alignment, while the inverse is not true. Ugh.
I've got a `.rustfmt.toml` file in all my repos with
Yep, but because it's not the default, plenty of ecosystem tooling just does not properly track the two separate types of leading whitespace (indentation vs alignment) and will happily conflate every tab_width characters of alignment with an indentation level (which is grossly incorrect). I don't have an example off the top of my head because I run very far each time it happens.
> Under heavy allocation, P99 latency tails are noticeably worse than a Rust equivalent that simply doesn’t allocate on the hot path.
Lmao so not an equivalent then? Standard glibc malloc, which is default in rust, will also similarly degrade albeit for different reasons.
Rust is great. However in an agentic world go will win. Look no further than incremental build times. This, combined with high token costs mean that for a given application it simply will cost more to to write it in Rust than Go.
This can easily be justified for many usecases, but for your vanilla crud app, do you really need Rust?
Per the article, you are getting 20-50% better more performance with Rust. Not worth it unless your team was already fluent in Rust. Now consider a scenario where your team uses AI exclusively to code, now you are spending more time and tokens waiting around to consume large rust builds. As far as I know this is an inherent property of Rust to have its safety guarantees.
I think Rust makes sense for a lot of cases, but for a small web service, overkill and unnecessary imho. If someone ported their crud app from Go to Rust I would question their priorities.
Again I am speaking more in terms of software engineering economics than anything else. Yes, I know in a perfect world Rust binaries are smaller, performance is better and code more “correct”, but the world is hardly perfect. People have to push code quickly, iterate quickly. Teams have churn, Rust, frankly is alien for many, etc.
Because the agentic world involves the generation of so much code that gets harder to review, I would think the compile-time guarantees of Rust would make it a better option.
This is true if the token budget and time are not taken into account. In practice though, waiting minutes instead of seconds per build multiplied by prompt and again by change adds up very fast.
Incremental Rust builds are almost never minutes (on recentish hardware)
A quick measurement on my web browser project with almost 600 dependencies:
- A clean "cargo check" was 31s
- An incremental "cargo check" with a meaningful change was 1.5s
Building is a little slower:
- A clean "cargo build" was 56.01s
- An incremental "cargo build" was 4s
But I find that LLMs are mostly calling "check" on Rust code.
---
That's on an Apple M1 Pro. The latest M4/M5 machines as ~twice as fast.
I mean i wouldnt call a 100% a little slower wrt check vs build. In any case, the more you change the longer the incremental check or build will take.
Sure, but when we're talking single-digit seconds it feels not that significant regardless?
My point is that it isn't necessarily that fast. It is relative to the amount of changes and where they were made. For a fair comparison you would also have to present the worst case incremental build time which approaches the full build time (this goes for Go too), which per your own example is nearly a minute for rust.
> For a fair comparison you would also have to present the worst case incremental build time which approaches the full build time (this goes for Go too)
The worst case that would approach a non-incremental build time would be if you were editing a leaf crate. But in almost all cases the leaf crates are 3rd-party dependencies that you would never edit directly.
A real-world worst case is probably more like ~10-20% of an non-incremental builds.
1.5s for a massive project, on a laptop,like the OP said is still barely anything in the context of agentic coding. It’s less than a single percentage point of the total time in the loop, even if the agent has to compile multiple times.
This is cope.
I do give you that rust is more verbose and thus more token heavy. However that verbosity is meaningful and the LLM would have to spend tokens thinking about the code to understand less verbose languages. So I’d consider that a wash - in some cases it hurts and in some it helps.
We don’t know how massive the project is, but in any case building and immediately building again of course will be fast. How fast is it if all files have a single line changed, for example refactoring a log message?
Not to mention we haven't even gotten to discussing tests.
> in any case building and immediately building again of course will be fast
FWIW, the compile time test above was done comparing consecutive commits. Which in this case happened to have ~3-4 lines changed.
When everyone is armed with Mythos-like hacking ability, it's hard for me to imagine people wouldn't make the tradeoff of security over price.
> spending more time and tokens waiting around
Can you clarify how you're spending tokens on waiting? My understanding is that the LLM isn't actually necessarily doing anything while a build runs. The whole process end to end may take longer for sure (ignoring things like the compiler catching more errors, that's really hard to factor in) but how does that correlate to more tokens?
> The whole process end to end may take longer for sure (ignoring things like the compiler catching more errors, that's really hard to factor in) but how does that correlate to more tokens?
This. rust emits more information both in its output and the syntax itself more complicated requires more tokens.
Your agent doesn't know how to use grep?
The cost of verbose compiler output surely cannot compare to the cost of shipping bugs that would've been caught at build time.
Indeed, but is it the case that all bugs you have are those in which would be caught by the compiler? It’s not like rust code inherently is bug free.
Of course, there's plenty of bugs in Rust code still. The fact that safe Rust should be able to statically guarantee entire classes of bugs like data races are impossible is a huge deal, though. We're totally free to have different values when it comes to what matters, but compile time and a verbose toolchain are not high costs for that, to me. I personally would first consider other things like the cognitive overhead of learning to work with the borrow checker.
> As far as I know this is an inherent property of Rust to have its safety guarantees.
From what I've seen, Rust's strictness is actually a huge win for LLMs, as they get much better feedback on what's wrong with the code. Things like null checking that would be a runtime error in Go are implied by the types / evident in the syntax in Rust.
Can you explain a bit about why token costs would favor Go and not Rust?
Go is more verbose, but Rust have more complex syntax which in practice require more tokens.
The big thing though is because builds are slower, you will end up waiting longer as tests are modified, rebuilt and run. This difference piles up fast.
Waiting longer for tests / builds doesn't have effect on token usage..
Rust's compile time is longer because the compiler does much more. And therefore the binaries are often smaller, start and run faster than Go
> However in an agentic world go will win
This is Silicon Valley fantasy.
It's a good thing then, that the AI hype is dying outside of ycombinator, the silicon valley and the US
As someone with a background of consulting in the Stockholm based gaming industry for the last decade+, I have to respectfully disagree. Nearly everyone I know is very much on the hype train. And for good reason too! The capabilities are undeniable!
The capabilities are very much deniable. They do not exist. Using LLMs to write code is either going to make you slower (as you have to review all that code), or make your software suck (because they write bad code and you're choosing to not review it). There's no actual win to be had here.
As is the hype.
You know, shovels are useful, they are just more useful to the shovel manufacturer than the gold diggers.
But in the end it's a cool tool that made it way easier to dig holes and tend to your garden!
Oh yeah, definitely. There has indeed been a lot of hype overestimating the capabilities. People thinking you can one-shot big complex applications with a few paragraphs of descriptions for example. There has also been a lot of anti-hype, or whatever you call it when people seem to believe LLMs don't provide any value for software Dev, basically writing all capabilities off as pure hype.
The truth of course is somewhere in the middle.
It's difficult to tell what people mean when they say hype sometimes.
So you're not using a shovel to maintain your garden?!