Remix.run Logo
Animats 2 hours 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.

atombender 32 minutes ago | parent | next [-]

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

fpoling 2 hours ago | parent | prev | next [-]

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.

hedgehog an hour ago | parent | next [-]

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.

fpoling 17 minutes ago | parent [-]

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.

throwaway894345 13 minutes ago | parent | prev [-]

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?

fpoling 4 minutes ago | parent [-]

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.

mikeocool 2 hours ago | parent | prev | next [-]

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.

boccko an hour ago | parent [-]

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.

the__alchemist 2 hours ago | parent | prev | next [-]

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

lionkor 2 hours ago | parent | prev | next [-]

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

fweimer 2 hours ago | parent | next [-]

Surely you need an alternative to Box<dyn Error> for reporting memory allocation failures?!

dwattttt an hour ago | parent [-]

A &(dyn Error + 'static) should be fine for that; you don't need any allocated/variable sized data in a memory allocation failure.

BobbyJo 2 hours ago | parent | prev [-]

Having many semantic options for error usage is functionally the same as having many error types, except worse.

ViewTrick1002 2 hours ago | parent [-]

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

iknowstuff 2 hours ago | parent | prev | next [-]

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.

tptacek an hour ago | parent [-]

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.

dwattttt an hour ago | parent [-]

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.

the__alchemist 5 minutes ago | parent [-]

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)

LoganDark 32 minutes ago | parent | prev | next [-]

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

innocentoldguy 42 minutes ago | parent | prev | next [-]

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.

LtWorf 2 hours ago | parent | prev [-]

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.

awesome_dude 2 hours ago | parent [-]

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