9 ms·
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 t
by Animats 5mo ago
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.
- iknowstuff 5mo agoRust 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.
- tptacek 5mo agoHe'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.
- dwattttt 5mo agoI 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.
- the__alchemist 5mo agoWhat'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)
- dwattttt 5mo agoBy 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.
- iknowstuff 5mo agoThere is no team io::Error. There is only one standard: https://doc.rust-lang.org/core/error/trait.Error.html https://doc.rust-lang.org/core/error/trait.Error.html
- awesome_dude 5mo agoNot 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
- lionkor 5mo ago> 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.
- BobbyJo 5mo agoHaving many semantic options for error usage is functionally the same as having many error types, except worse.
- ViewTrick1002 5mo agoThey 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/anyhow https://crates.io/crates/thiserror https://crates.io/crates/thiserror
- throwaway894345 5mo agoI’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?
- LtWorf 5mo agoPraising 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.
- awesome_dude 5mo agoLinters 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
- ventana 5mo agoC, 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.
- awesome_dude 5mo agoYeah - I assumed so - which makes the GP post... bizarre
- LtWorf 5mo agoSame as in go, a language designed several decades later.
- the__alchemist 5mo agoI 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).
- marcus_holmes 5mo agoConversely, 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).
- the__alchemist 5mo agoInteresting! 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?
- marcus_holmes 5mo agoWe 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.
- fpoling 5mo agoFor 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.
- hedgehog 5mo agoI'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.
- fpoling 5mo agoJava 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.
- hedgehog 5mo agoPossibly in your specific application, usually there are a handful of options far less painful than a rewrite. For the original issue of GC pauses, a narrow change is to move problem data to non-pointer-carrying types, or the bigger hammer of manually managed slices of those types. The second helps with fragmentation too. Some workloads can be split into multiple processes as a direct way to have smaller heaps. If none of those options are enough then off-heap storage lets you do whatever you want. I do have some complaints about Go, but one of the big ones has been fixed since I last wrote much Go code and it seems like a fine choice for a lot of applications.
- throwaway894345 5mo agoIsn’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?
- mikeocool 5mo agoI 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.
- boccko 5mo agoStandard 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.
- j1elo 5mo ago> 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.
- troad 5mo agoPerhaps 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.
- innocentoldguy 5mo agoI 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.
- stock_toaster 5mo agoOr gleam if you don’t fancy elixir.
- LoganDark 5mo agothiserror 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).
- kelnos 5mo agothiserror and anyhow are std::error::Error with fewer steps.
- LoganDark 5mo agoThey're external crates that either generate Error implementations (thiserror) or act as dynamic wrappers implementing Error (anyhow), so they're more than a simple hand-written implementation. But the developer experience of pulling thiserror or anyhow off the shelf can certainly be more convenient than the hand-written implementation, sure.
- atombender 5mo agoFor backend web dev, there are advantages. I really like Axum's use of typing: pub async fn dataset_stats_handler( Path(dataset_id): Path<String>, Query(verbose): Query<bool>, ) -> impl IntoResponse { ... } With a route like: .route("/datasets/{dataset_id}/stats", get(dataset_stats_handler)) …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.
- ricardobeat 5mo agogo is slightly more verbose (surprise) but you can achieve the same thing using struct binding in gin: type DatasetStatsQuery struct { Verbose bool `form:"verbose"` } func DatasetStatsHandler(c *gin.Context) { datasetID := c.Param("dataset_id") var query DatasetStatsQuery if err := c.ShouldBindQuery(&query); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // query.Verbose == bool }} This is actually a great example - what happens in that Rust version when the input parsing fails? Go makes it explicit.
- sheept 5mo agoI'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.QueryRejection.html https://docs.rs/axum/latest/axum/extract/rejection/enum.Quer...
- sieabahlpark 5mo ago[dead]
- xyzzy_plugh 5mo ago
- tertasrfg 5mo agoThe funny thing is: All Rust source code looks like an assembly syntax error.
- winrid 5mo agoIf you spend an hour skimming the Rust Book it's not really that bad
- skydhash 5mo agoIt'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.
- winrid 5mo agoRust has autocomplete even in Sublime Text with an LSP... not quite as good as with Java, but I don't write either by hand much anymore.
- what 5mo ago[flagged]
- runhelm 5mo ago[flagged]
- mountainriver 5mo agoI 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
- bottlepalm 5mo agoFor 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.
- Foobar8568 5mo agoThe build time and space for rust is awful for fast iteration e.g change a thing, verify.
- lofties 5mo agoReading the code? Who has the time. Aah, I am sure the chickens of vibe coded origin, will never come to roost.
- kaashif 5mo agoTime isn't the constraint here, but ability. Someone complaining about how hard Rust is to write is probably not capable of reviewing Rust code very well. The usual reaction or opinion from e.g. good C++ programmers switching to Rust is that the added guardrails and expressivity are great and make things easier.
- pas 5mo agoI find it easier to review Rust (but Go too) than to write it.
- pjmlp 5mo agoFor 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.
- swiftcoder 5mo ago> 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 anyhow explicitly isn't designed for what you are trying to do here. It's designed to be the last link in the chain (and complementary to thiserror, not in competition). If you are using anyhow any deeper than your top-level binary crate, you are likely to be in for an unpleasant time.
- IshKebab 5mo agoHe was talking about the chain of function calls not crates. You still have that in your top level crate.
- wtetzner 5mo agoExcept anyhow is compatible in that direction. io::Error and thiserror types will automatically convert to anyhow errors.
- IshKebab 5mo agoIn my experience it's not quite as automatic as you'd hope. I always often to throw in stuff like `.map_err(Into::into)` or `Ok(foo?)` to convert the errors. Not the end of the world but I do think defending the proliferation of error types in typical Rust projects is copium.
- wtetzner 5mo agoYes, `?` automatically does `.into()`. There are plenty of reasons you can complain about Rust's error handling, but "proliferation of error types" feels like a misunderstanding of how Rust error handling works in the first place.
- mswphd 5mo agoalso thiserror isn't incompatible with io::Error? all thiserror does is do code generation for "typical" enum errors. The errors it generates are normal rust code though, and the fact that they're generated by thiserror shouldn't really matter?
- Ygg2 5mo agoGo also has one glaring disadvantage. GC. Assuming you're in the cloud. It's easy to write code that trivially eats memory. Plus any resources spent on it, are resources not spent on other cloud provider things.
- vorticalbox 5mo agoI don't fully get the argument about errors. in rust say a function returns Result<T, E> so either the we get a result all an error how is that different from (int, err) in go? do you not still need to handle the error? in go you just return the error up to whatever the top caller is.
- ssfak 5mo agoIn Go you can ignore the error value though, and use directly the returned value (`int` in your example). In Rust you cannot do that, you need to unwrap the Result or use the `?`
- ben-schaaf 5mo agoIf the returned value is still valid despite an error, then the function would return (u32, Option<Error>), perfectly valid rust. If the value is meaningless in case of an error then using it is incorrect code; you wouldn't want to do that in either language and rust makes that assumption explicit with unwrap. If you want a default value in case of error just use unwrap_or_default.
- moltonel 5mo agoGo funcs can return both a value and an error, or neither, it's a common gotcha. Having to check the behavior each time is no fun. Missing error handling is checked at compile-time in Rust (lint-time in Go), and can be enabled for any struct or function (https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-must_use-attribute https://doc.rust-lang.org/reference/attributes/diagnostics.h...), not just `Result<T,E>`. Returning an error to the caller in Rust can be done with a single character.
- DeathArrow 5mo ago>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. Now there is a cult of rewriting everything in Rust. System level software? Yes. Web? I prefer not to.
- christophilus 5mo agoI just hate how many dependencies you have to pull in for a typical Rust project vs Go. As far as Go being an ugly language, there are some interesting wrappers that put lipstick on that pig such as https://lisette.run/ https://lisette.run/ But personally, I don’t mind Go at all. I’ve even begun to prefer it for some things. That may be Stockholm syndrome, though.
- phplovesong 5mo agoBeen playing with Lisette for a few weeks, so far im really liking it. Think is has some potential.
- guilhas 5mo agoRust is easily one of the worst languages to look at
- apatheticonion 5mo agoI use Rust for web services all the time. It's a dream compared to Go (which I wrote professionally for years). At this point, I can't imagine a scenario not to use Rust for writing a web API.
- tracker1 5mo agoDunno... I really like FastEndpoints in C# a lot as well as Hono in JS/TS environments. That said, I do like Axum in Rust. All for varying reasons though.
- throwaway2037 5mo agoWhat is "formal QA support"?
- malcolmgreaves 5mo ago> Rust lacks a uniform error type. Not quite true. The unifying error trait is std::error::Error. > pain when you have to pass them upward through a chain of calls Kind of? You just make an enum with the various variants that need to be passed through and use the #[from] macro to generate the conversion code automatically. It’s more characters than eg. A union type in Python or TypeScript, but it’s not much more. Plus, it makes you think about your error design, which is important!
- deleted 5mo ago[deleted]
- waterTanuki 5mo ago> 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. nit: Rust has three main Error implementations not systems. std::error::Error is a trait because it's left up to the user to provide project-specific error types, even if those types are anyhow::Errors. The system follows any implementation you use: ? syntax, .unwrap(), .expect(), handling, formatting, etc. I've never had a problem using anyhow to pass errors up the call stack in any project I've used it in. What exactly is a pain?