Go is a bit unique a it has a really substantial stdlib, so you eliminate some of the necessary deps, but it's also trivial to rely on established packages like Tokio etc, vendor them into your codebase, and not have to worry about it in the future.
The point is someone needs to curate those "deps". It's not about rolling your own, it's about pulling standard stuff from standard places where you have some hope that smart people have given thought to how to audit, test, package, integrate and maintain the "deps".
NPM and Cargo and PyPI all have this disease (to be fair NPM has it much worse) where it's expected that this is all just the job of some magical Original Author and it's not anyone's business to try to decide for middleware what they want to rely on. And that way lies surprising bugs, version hell, and eventually supply chain attacks.
The curation step is a critical piece of infrastructure: thing things like the Linux maintainer hierarchy, C++ Boost, Linux distro package systems, or in its original conception the Apache Foundation (though they've sort of lost the plot in recent years). You can pull from those sources, get lots of great software with attested (!) authorship, and be really quite certain (not 100%, but close) that something in the middle hasn't been sold to Chinese Intelligence.
But the Darwinian soup of Dueling Language Platforms all think they can short circuit that process (because they're in a mad evangelical rush to get more users) and still ship good stuff. They can't.
Its STD exists because Go is a language built around a "good enough" philosophy, and it gets painful once you leave that path.
It is also common in languages without package managers to rely on the distro to provide the package, which adds a level of scrutiny.
Mucking around with cmake adds enough friction that everyone can take a beat for thoughtful decision-making.
It’s not that unique though. I can say that Python and hell, even PHP have pretty complete but also well documented stdlib.
Java is meh tier but C# is also pretty good in this aspect.
It’s totally a choice for Rust not to have a real stdlib and actually I feel like that would maybe make Rust maybe the best language overall.
In a decade or so Go the awkward things about Go will have multiplied significantly and it'll have many of the same problems Python currently has.
Big caveat that this is just for me personally, but uv has fixed this for me personally. Game changing improvement for Python. Appropriately, uv is written in rust.
The stdlib packages are far better designed in Go than in Python. “The standard library is where packages go to die” is literally not a thing in Go, in fact quite the opposite.
Edit: changed "perfect" to "improve", as I meant "perfect" as "betterment" not in terms of absolute perfection.
At least it is my experience building some systems.
Not sure it is always a good calculus to defer the hard thinking to later.
For stuff in the standard library proper, the versioning system is working well for it. For example, the json library is now at v2. Code relying on the original json API can still be compiled.
I like go's library it's got pretty much everything needed out of the box for web server development. Backwards compatibility is important too.
I was thinking of this quote from the article:
> Take it or leave it, but the web is dynamic by nature. Most of the work is serializing and deserializing data between different systems, be it a database, Redis, external APIs, or template engines. Rust has one of the best (de)serialization libraries in my opinion: serde. And yet, due to the nature of safety in Rust, I’d find myself writing boilerplate code just to avoid calling .unwrap(). I’d get long chain calls of .ok_or followed by .map_err. I defined a dozen of custom error enums, some taking other enums, because you want to be able to handle errors properly, and your functions can’t just return any error.
I was thinking: This is so much easier in Haskell.
Rather than chains of `ok_or()` and `map_err()` you use the functor interface
Rust:
``` call_api("get_people").map_or("John Doe", |v| get_first_name(v)).map_or(0, |v| get_name_frequency(v)) ```
Haskell:
``` get_first_name . get_name_frequency <$> callApi "get_people" ```
It's just infinitely more readable and using the single `<$>` operator spares you an infinite number of `map_or` and `ok_or` and other error handling.
However, having experience in large commercial Haskell projects, I can tell you the web apps also suffer from the dreaded dependency explosion. I know of one person who got fired from a project due to no small fact that building the system he was presented with took > 24 hours when a full build was triggered, and this happened every week. He was on an older system, and the company failed to provide him with something newer, but ultimately it is a failing of the "everything and the kitchen sink" philosophy at play in dependency usage.
I don't have a good answer for this. I think aggressive dependency reduction and tracking transitive dependency lists is one step forward, but it's only a philosophy rather than a system.
Maybe the ridiculous answer is to go back to php.
Incremental Nix builds can take less than 1 munute to build everything, including the final deployable docker image with a single binary on very large Haskell codebases. That fact the the person was fired for everybody around him systematically failing to admit and resolve a missing piece of supportive infrastructure for the engineering effort of one person tells a lot about the overall level of competence in that team.
> but ultimately it is a failing of the "everything and the kitchen sink" philosophy at play in dependency usage.
Not really, as the kitchen sink only has to build once per its version change, for all future linkage with your software for the entire engineering team doing the builds in parallel.
When I update the rust compiler, I do so with very little fear. My code will still work. The rust stdlib backwards compatible story has been very solid.
Updating the Go compiler, I also get a new stdlib, and suddenly I get a bunch of TLS version deprecation, implicit http2 upgrades, and all sorts of new runtime errors which break my application (and always at runtime, not compiletime). Bundling a large standard library with the compiler means I can't just update the tls package or just update the image package, I have to take it or leave it with the whole thing. It's annoying.
They've decided the go1 promise means "your code will still compile, but it will silently behave differently, like suddenly 'time1 == time2' will return a different result, or 'http.Server' will use a different protocol", and that's somehow backwards compatible.
I also find the go stdlib to have so many warts now that it's just painful. Don't use "log", use "log/slog", except the rest of the stdlib that takes a logger uses "log.Logger" because it predates "slog", so you have to use it. Don't use the non-context methods (like 'NewRequest' is wrong, use 'NewRequestWithContext', don't use net.Dial, etc), except for all the places context couldn't be bolted on.
Don't use 'image/draw', use 'golang.org/x/image/draw' because they couldn't fix some part of it in a backwards compatible way, so you should use the 'x/' package. Same for syscall vs x/unix. But also, don't use 'golang.org/x/net/http2' because that was folded into 'net/http', so there's not even a general rule of "use the x package if it's there", it's actually "keep up with the status of all the x packages and sometimes use them instead of the stdlib, sometimes use the stdlib instead of them".
Go's stdlib is a way more confusing mess than rust. In rust, the ecosystem has settled on one logging library interface, not like 4 (log, slog, zap, logrus). In rust, updates to the stdlib are actually backwards compatible, not "oh, yeah, sha1 certs are rejected now if you update the compiler for better compile speeds, hope you read the release notes".
> Don't use "log", use "log/slog", except the rest of the stdlib that takes a logger uses "log.Logger" because it predates "slog", so you have to use it.
What in the standard library takes a logger at all? I don't think I've ever passed a logger into the standard library.
> the ecosystem has settled on one logging library interface, not like 4 (log, slog, zap, logrus)
I've only seen slog since slog was added to the standard library. Pretty sure I've seen logrus or similar in the Kubernetes code, but that predated slog by a wide margin and anyway I don't recall seeing _any_ loggers in library code.
> In rust, the ecosystem has settled on one logging library interface
I mean, in Rust everyone has different advice on which crates to use for error handling and when to use each of them. You definitely don't have _more standards_ in the Rust ecosystem.
This is not always true, as seen with rustc 1.80 and the time crate. While it only changed type inference, that still caused some projects like Nix a lot of trouble.
1. The web standard APIs themselves 2. It's own standard library inspired by Go's standard library (plus some niceties like TOML minus some things not wanted in a JS/TS standard library since they're already in the web standard APIs) 3. Node's standard library (AKA: built-in modules) to maintain backwards compatibility with node.
Bun has 1 and 3, and sort of has it's own version of 2 (haphazard, not inspired by go, and full of bun-isms which you may like but may not, but standard database drivers is nice).
I get that cross platform desktop app is a complicated beast but it gives off those creepy npm vibes.
A little copying is better than a little dependency.If a bug in your system means silent data corruption that nobody notices for a week — and I've lived this — Rust is worth every second of compile time. If a bug means a 500 and you redeploy, you're paying for insurance you don't need. Different worlds, different tools.
The thing I actually love about Rust — and this sounds weird — is how it handles failure. In C, every function call is an implicit "and also maybe something went horribly wrong, but let's just hope it didn't." You get used to it. You stop seeing it. Then one day you're staring at a corruption bug and you trace it back to an error return that got silently swallowed six call sites ago, and you feel physically ill. Result types are annoying when you're validating form input. They're a gift from god when you're the one who has to explain why someone's data is gone.
But yeah, for web stuff? Just use TypeScript. Life's too short to fight the borrow checker over a blog.
I am not sure about TypeScript. I think having static typing is just too good of an insurance against stupid bug and for your own sanity. I think for web purposes, especially with LLM around, you probably should just use Go. You don't have to like it, but there's enough training dataset for your CRUD application. So all you really need to do is to be able to read it.
TypeScript has static typing though?
but the truth is that Rust is not meant for everything. UI is an abstraction layer that is very human and dynamic. and i can come and say, “well, we can hide that dynamism with clever graph composition tricks” à la Elm, React, Compose, etc, but the machinery that you have to build for even the simplest button widget in almost every Rust UI toolkit is a mess of punctuation, with things like lifetimes and weird state management systems. you end up building a runtime when what you want is just the UI. that’s what higher level languages were made for. of course data science could be done in Rust as well, but is the lifetime of the file handle you’re trying to open really what you’re worried about when doing data analysis?
i think Rust has a future in the UI/graphics engine space, but you have to be pretty stubborn to use it for your front end.
There are real advantages to choosing a jack of all trades language for everything; for example it makes it easier for an engineer on one part of your project to help out on a different part of your project.
But it sounds like the OP didn't get any of the benefits of "jack of all trades", nor did he choose a field where Rust is "master of some".
speak for yourself :-)
But sadly Java can't rule them all because there is no browser story and no IOS story.
This can be a double edged sword. Yes, languages like python and typescript/JavaScript will let you not catch an exception, which can be convenient. But that also often leads to unexpected errors popping up in production.
The times something like that happened to me AND wasn't a trivial fix can be counted on half a hand. A tradeoff I'd take any day to not have to deal with rust all of the time.
Having a long ass chain of 10 nested exceptions might be overwhelming to a beginner, but an experienced developer knows which types of exceptions are caused by what and instinctively tunes out the irrelevant ones and goes straight to the source of the problem since the stack trace directly tells you which chain of calls caused the issue.
I was trying to build an HTML generator in Rust and got pretty far, but I don't think I'll ever be happy with the API unless I learn some pretty crazy macro stuff, which I don't want. For the latter project, the "innovation tokens" really rings true for me, I spent months on the HTML gen for not much benefit.
You have very mature webservers, asyncio, ORMs, auth, etc., it's very easy to write, and the type safety helps a ton.
In 2020 it might have taken some innovation tokens, but the only things that require a ton less (for web backend) are probably Java, python, and node.js, and they all have their unique pain points that it doesn't seem at all crazy?
Your reply made me curious about ORMs, btw. Which one would you recommend? Maybe things have improved since I last checked. Last time I didn't like any of them and ended up settling on `sqlx` + hand-written SQL (the code is open source, hosted at https://github.com/rustls/rustls-bench-app/tree/main/ci-benc...).
I find C# can be a really good middle ground on the backend (not a blazor fan)... the syntax and expressiveness improves with every release. You can burrow as lot of patterns from the likes of Go as well as FP approaches. What I don't care for are excessively complex (ie: "Enterprise") environments where complexity is treated like a badge of honor instead of the burden of spaghetti that it is in practice.
Edit to add: It might not be an imperative language, but having written some HTML and asked the computer to interpret it, the computer now has a programmed capability, determined by what was written, that's repeatable and that was not available apart from the HTML given. QED.
HTML is a markup language, it's even in the name... but it's not a complete programming language by any stretch.
Typescript is pretty type-safe, and it's perfectly integrated with hot code reload, debuggers, and all the usual tools. Adding transpilation in that flow only creates friction.
That's also why things like Blazor are going nowhere. C# is nicer than Typescript, but the additional friction of WASM roundtrips just eats all the advantage.
Blazor performance is around 3x slower than React, it'll use 15-20x more RAM, and it's 20x larger over the wire. I think if Blazor could match React performance, it'd be quite popular. As it stands, it's hard to seriously consider it for something where users have other options.
Microsoft has been working to make C#/.NET better for AOT compilation, but it's tough. Java has been going through this too. I don't really know what state it's at, but (for example) when you have a lot of libraries doing runtime code generation, that's fine when you have a JIT compiler running the program. Any new code generated at runtime can be run and optimized like any other code that it's running.
People do underappreciate the JS/TS ecosystem, but I think there are other reasons holding back stuff running on WASM. With Blazor, performance, memory usage, and payload size are big issues. With Flutter and Compose Multiplatform, neither is giving you a normal HTML page and instead just renders onto a canvas. With Rust, projects like Dioxus are small and relatively new. And before WASM GC and the shared heap, there was always more overhead for anything doing DOM stuff. WASM GC is also pretty new - it's only been a little over a year since all the major browsers supported it. We're really in the infancy of other languages in the browser.
I have been an anti Typescript guy for a long time but I wouldn't deny for a moment that it's probably by far the most mature ecosystem.
ASP.NET MVC alongside JS/TS frameworks does the job just fine, as does Spring, Quarkus and co.
> Similar thing can be said about writing SQL. I was really happy with using sqlx, which is a crate for compile-time checked SQL queries. By relying on macros in Rust, sqlx would execute the query against a real database instance in order to make sure that your query is valid, and the mappings are correct. However, writing dynamic queries with sqlx is a PITA, as you can’t build a dynamic string and make sure it’s checked during compilation, so you have to resort to using non-checked SQL queries. And honestly, with kysely in Node.js, I can get a similar result, without the need to have a connection to the DB, while having ergonomic query builder to build dynamic queries, without the overhead of compilation time.
I've used sqlx, and its alright, but I've found things much easier after switching to sea-orm. Sea-orm has a wonderful query builder that makes it feel like you are writing SQL. Whereas with sqlx you end up writing Rust that generates SQL strings, ie re-inventing query builders.
You also get type checking; define your table schema as a struct, and sea-orm knows what types your columns are. No active connection required. This approach lets you use Rust types for fields, eg Email from the email crate or Url from the url crate, which lets you constrain fields even further than what is easy to do at the DB layer.
ORMs tend to get a bad reputation for how some ORMs implement the active record pattern. For example, you might forget something is an active record and write something like "len(posts)" in sqlalchemy and suddenly you are counting records by pulling them from the DB in one by one. I haven't had this issue with sea-orm, because it is very clear about what is an active record and what is not, and it is very clear when you are making a request out to the DB. For me, it turns out 90% of the value of an ORM is the query builder.
Ceci n'est pas une pipe
And, IMO, making dynamic queries harder is preferable. Dynamic queries are inherently unsafe. Sometimes necessary, however you have to start considering things like sql injection attacks with dynamic queries.
This isn't to poo poo sea-orm. I'm just saying that sqlx's design choice to make dynamic queries hard is a logical choice from a safety standpoint.
Depends on what you mean by "dynamic query". You are dealing with injection attacks as soon as you start taking user input. Most useful user facing applications take user input.
In a simple case it might be "SELECT * FROM posts WHERE title LIKE '%hello world%', where "hello world" is a user specified string. This is easy with sqlx. Where things get more difficult is if you want to optionally add filters for things like date posted, score of the post, author, etc... That makes the query dynamic in a way that can't be solved by simply including a bind.
That's where sea-orm shines over sqlx IMO. sqlx will force you to do something like
```
let mut my_query = "SELECT * FROM posts WHERE title LIKE '%' + $1 + '%'";
let mut my_binds = vec![args.keyword];
if let Some(date) = args.date {
my_query = format("{my_query} AND date = $2");
my_binds.push(date);
}...
```
Your building a string and tracking binds. It gets messy. A good query builder like seaorm has lets you do something this:
```
let mut query = Posts::find().filter(Column::title::like(args.keyword));
if let Some(date) = args.date {
query = query.filter(column::Date::eq(date));
}```
This pays off as your queries get more complicated. It pushes the string manipulation and bookkeeping into a library, which can be more thoroughly tested.
It also lets you pass around typed partial queries, eg in the example above query might be returned from a function, which helps you build more modular code.
Unmounting and then remounting the same component is actually a bad thing when you lose your component state in the process. And when you have enough useEffect's in your system that's exactly what happens unless you're liberally sprinkling useMemo
There is ts-rs [1], but it's only for TS.
And even then I do it by serving JSON API's and not by serving HTML.
I've supported backends in typescript, python, Java, and Rust.
Rust pages me the least at night. Sleep is beautiful.
I agree with you. Rust is rock-solid. I had zero crashes with Rust. But, having said that, I so-far have zero crashes with Node.js as well. Maybe because I'm a one man team, and I'm very pedantic, so everything is wrapped in try/catch, schema validations, and strict typescript/eslint rules.
I would agree with you that *by default*, Rust makes it harder to write bad/bug prone code compared to others, but with discipline (which big teams in "fast moving environments" usually don't have), you can get similar assurances with Node/Typescript.
Use it where it is ideal, system programming level tasks where for whatever reasons automatic memory management is either not possible, or not wanted for various reasons.
This whole paragraph is so true. The last couple of years have been pretty rough in Node land.
Use a (statically typed) GC language for that.
Personally would look at Go over Node as it gets a lot of the systems language patterns without the overhead.
That seems much more like the future than embracing Node... <emoji here>
Idk, it just feels like OP chose all the wrong approaches with Rust, including using a separate language and ecosystem for the frontend, which is where most of the friction comes from. For example, Dioxus is a React clone that is somehow leagues better than React (and Next.js, too), and it has hot-reloading that brings compiles down to subsecond times, which makes building UI with it just as productive as with Node / Vite etc. I use it for server side code as well and it's great. Compilation times can be an issue with Rust, it's something I miss from Go, but there are ways to improve on it, and just being smart about what deps you include, avoiding overuse of macros etc can make a difference. I know these things were not around when OP started using Rust for their application, but they are around now.
Node and TS are quite frankly inferior to Rust in most ways. Bad language, ecosystem full of buggy unmaintained packages with the worse security profile of all the common languages, no unified build tooling that seems to break your project every 6 months, constant churn of blessed frameworks and tools, an stdlib that is not much more comprehensive than Rust's and outright broken in some ways, at least three different approaches to modules (esm, commonjs, umd, and more...?), I could go on an on. There is a reason why everyone seemingly reinvents the wheel in that ecosystem over and over again -- the language and platform is fundamentally not capable of achieving peoples goals, and every solution developed comes with massive tradeoffs that the next iteration attempts to solve, but that just creates additional issues or regressions for future attempts to tackle.
I've been using Rust with Dioxus and was completely mind blown when I started with it. With barely knowing any Rust (just React) I was able to jump right in and build with it, somehow it was more intuitive to me than most modern JS full stack frameworks. It seemingly already has most if not all of the features that similar JS frameworks have been developing for years, and because it's written in Rust things like conditional compilation are built into the language instead of being a third party babel plugin. That helps to remove a ton of friction. And it's trivial to build those same apps for desktop and mobile as well, something that's basically not possible with the JS frameworks.
Even stuff like websockets, go try to implement a type safe web socket connection with a server and client in Next.js or Astro. You'll need a ws library, something like Zod for validation, etc. In Rust it's just:
#[derive(Serialize, Deserialize, Clone, Default)]
enum SocketMessage { Hello(id: i32) }
#[get("/api/ws")]
async fn web_socket(options: WebSocketOptions) -> Websocket<SocketMessage> {
options.on_upgrade(move |mut socket| async move {
while let Ok(msg) = socket.recv().await {
match msg { SocketMessage::Hello(id) => {} } // handle messages
}
})
}
fn App() -> Component {
let mut socket = use_websocket(web_socket);
rsx!{ button { onclick: move || socket.send(SocketMessage::Hello(42), "say hello" } }
} let mut breed = use_signal(|| "hound".to_string());
let dogs = use_resource(move || async move {
reqwest::Client::new()
.get(format!("https://dog.ceo/api/breed/{breed}/images"))
.send()
.await?
.json::<BreedResponse>()
.await
});
rsx! {
input {
value: "{breed}",
oninput: move |e| breed.set(e.value()),
}
div {
display: "flex",
flex_direction: "row",
if let Some(response) = &*dogs.read() {
match response {
Ok(urls) => rsx! {
for image in urls.iter().take(3) {
img {
src: "{image}",
width: "100px",
height: "100px",
}
}
},
Err(err) => rsx! { "Failed to fetch response: {err}" },
}
} else {
"Loading..."
}
}
}
Imo the RSX here is much less verbose than JSX. Inline match statement, inline if statement, inline for loop, .take(3) compared to `Array.from({ length: 3 }).map((_, i) => urls[i]))`, etc etc. This gives you automatic cancellation of the future, whereas with React you would need a third party library like React Query, and then manually abort requests in the asynchronous function with an abort signal -- in Rust, you get that for free. You also get data validation for free, instead of needing eg. Zod for manual runtime validation.