Thoughts on Go vs. Rust vs. Zig
sinclairtarget.com
sinclairtarget.com
Well, no, creating a mutable global variable is trivial in Rust, it just requires either `unsafe` or using a smart pointer that provides synchronization. That's because Rust programs are re-entrant by default, because Rust provides compile-time thread-safety. If you don't care about statically-enforced thread-safety, then it's as easy in Rust as it is in Zig or C. The difference is that, unlike Zig or C, Rust gives you the tools to enforce more guarantees about your code's possible runtime behavior.
Moving back to a language that does this kind of thing all the time now, it seems like insanity to me wrt safety in execution
Novices start slapping global variables everywhere because it makes things easy and it works, until it doesn't and some behaviour breaks because... I don't even know what broke it.
On a smaller scale, mutable date handling libraries also provide some memorable WTF debugging moments until one learns (hopefully) that adding 10 days to a date should probably return a new date instance in most cases.
This is a tombstone-quality statement. It's the same framing people tossed around about C++ and Perl and Haskell (also Prolog back in the day). And it's true, insofar as it goes. But languages where "trivial" things "just require" rapidly become "not so trivial" in the aggregate. And Rust has jumped that particular shark. It will never be trivial, period.
Sure. And in C and Zig, it's "trivial" to make a global mutable variable, it "just requires" you to flawlessly uphold memory access invariants manually across all possible concurrent states of your program.
Stop beating around the bush. Rust is just easier than nearly any other language for writing concurrent programs, and it's not even close (though obligatory shout out to Erlang).
And it's a good concept, because it makes people feel a bit uncomfortable to type the word "unsafe", and they question whether a globally mutable variable is in fact what they want. Which is great! Because this is saving every future user of that software from concurrency bugs related to that globally mutable variable, including ones that aren't even preserved in the software now but that might get introduced by a later developer who isn't thinking about the implications of that global unsafe!
If you treat shared state like owned state, you're in for a bad time.
Maybe, but the language being hard in aggregate is very different from the quoted claim that this specific thing is hard.
My understanding is that Rust prevents data races, but not all race conditions. You can still get a logical race where operations interleave in unexpected ways. Rust can’t detect that, because it’s not a memory-safety issue.
So you can still get deadlocks, starvation, lost wakeups, ordering bugs, etc., but Rust gives you:
- No data races
- No unsynchronized aliasing of mutable data
- Thread safety enforced through type system (Send/Sync)
This fits quite naturally in Rust. You can let your mutex own the pair: locking a `Mutex<(u32, u32)>` gives you a guard that lets you access both elements of the pair. Very often this will be a named `Mutex<MyStruct>` instead, but a tuple works just as well.
Because rust guarantees you won't have multiple exclusive (and thus mutable refs), you won't have a specific class of race conditions.
Sometimes however, these programs are very strict, and you need to relax these guarantees. To handle those cases, there are structures that can give you the same shared/exclusive references and borrowing rules (ie single exclusive, many shared refs) but at runtime. Meaning that you have an object, which you can reference (borrow) in multiple locations, however, if you have an active shared reference, you can't get an exclusive reference as the program will (by design) panic, and if you have an active exclusive reference, you can't get any more references.
This however isn't sufficient for multithreaded applications. That is sufficient when you have lots of pieces of memory referencing the same object in a single thread. For multi-threaded programs, we have RwLocks.
Rust approach to shared memory is in-place mutation guarded by locks. This approach is old and well-know, and has known problems: deadlocks, lock contention, etc. Rust specifically encourages coarse-granular locks by design, so lock contention problem is very pressing.
There are other approaches to shared memory, like ML-style mutable pointers to immutable data (perfected in Clojure) and actors. Rust has nothing to do with them, and as far as I understand the core choices made by the language make implementing them very problematic.
Logical race conditions and deadlocks can still happen.
They are to be used with caution. If your execution environment is simple enough they can be quite useful and effective. Engineering shouldn't be a religion.
> I can not count how many times I have been pulled in to debug some gnarly crash and the result was, inevitably, a mutable global variable.
I've never once had that happen. What types of code are you working on that this occurs so frequently?
There's lots of interest things you could do with a rust like (in terms of correctness properties) high level language, and getting rid of global variables might be one of them (though I can see arguments in both directions). Hopefully someone makes a good one some day.
If you use unsafe to opt out of guarantees that the compiler provides against data races, it’s no different than doing the exact same thing in a language that doesn’t protect against data races.
Second, unsafe means the author is responsible for making it safe. Safe in rust means that the same rules must apply as unsafe code. It does not mean that you don't have to follow the rules. If one instead used it to violate the rules, then the code will certainly cause crashes.
I can see that some programmers would just use unsafe to "get around a problem" caused by safe rust enforcing those rules, and doing so is almost guaranteed to cause crashes. If the compiler won't let you do something, and you use unsafe to do it anyway, there's going to be a crash.
If instead we use unsafe to follow the rules, then it won't crash. There are tools like Miri that allow us to test that we haven't broken the rules. The fact that Miri did find two issues in my crate shows that unsafe is difficult to get right. My crate does clever bit-tricks and has object graphs, so it has to use unsafe to do things like having back pointers. These are all internal, and you can use the crate in safe rust. If we use unsafe to implement things like doubly-linked lists, then things are fine. If we use unsafe to allow multiple threads to mutate the same pointers (Against The Rules), then things are going to crash.
The thing is, when you are programming in C or C++, it's the same as writing unsafe rust all the time. In C/C++, the "pocket of unsafe code" is the entire codebase. So sure, you can write safe C, like I can write safe "unsafe rust". But 99% of the code I write is safe rust. And there's no equivalent in C or C++.
I mean, it does. I'm not sure what you consider the default approach, but to me it would be to wrap the data in a Mutex struct so that any thread can access it safely. That works great for most cases.
> Perhaps mutable global variables are not a common use case.
I'm not sure how common they are in practice, though I would certainly argue that they shouldn't be common. Global mutable variables have been well known to be a common source of bugs for decades.
> Unsafe might make it easier, but it’s not obvious and probably undesired.
All rust is doing is forcing you to acknowledge the trade-offs involved. If you want safety, you need to use a synchronization mechanism to guard the data (and the language provides several). If you are ok with the risk, then use unsafe. Unsafe isn't some kind of poison that makes your program crash, and all rust programs use unsafe to some extent (because the stdlib is full of it, by necessity). The only difference between rust and C is that rust tells you right up front "hey this might bite you in the ass" and makes you acknowledge that. It doesn't make that global variable any more risky than it would've been in any other language.
I'm a Rust fan, and I would generally agree with this. It isn't difficult, but trivial isn't quite right either. And no, global vars aren't terribly common in Rust, and when used, are typically done via LazyLock to prevent data races on intialization.
> I don’t know Rust, but I’ve heard pockets of unsafe code in a code base can make it hard to trust in Rust’s guarantees. The compromise feels like the language didn’t actually solve anything.
Not true at all. First, if you aren't writing device drivers/kernels or something very low level there is a high probability your program will have zero unsafe usages in it. Even if you do, you now have an effective comment that tells you where to look if you ever get suspicious behavior. The typical Rust paradigm is to let low level crates (libraries) do the unsafe stuff for you, test it thoroughly (Miri, fuzzing, etc.), and then the community builds on these crates with their safe programs. In contrast, C/C++ programs have every statement in an "unsafe block". In Rust, you know where UB can or cannot happen.
This first-class representation of memory as a resource is a must for creating robust software in embedded environments, where it's vital to frontload all fallibility by allocating everything needed at start-up, and allow the application freedom to use whatever mechanism appropriate (backpressure, load shedding, etc) to handle excessive resource usage.
But for operating systems with overcommit, including Linux, you won't ever see the act of allocation fail, which is the whole point. All the language-level ceremony in the world won't save you.
You can impose limits per process/cgroup. In server environments it doesn't make sense to run off swap (the perf hit can be so large that everything times out and it's indistinguishable from being offline), so you can set limits proportional to physical RAM, and see processes OOM before the whole system needs to resort to OOMKiller. Processes that don't fork and don't do clever things with virtual mem don't overcommit much, and large-enough allocations can fail for real, at page mapping time, not when faulting.
Additionally, soft limits like https://lib.rs/cap make it possible to reliably observe OOM in Rust on every OS. This is very useful for limiting memory usage of a process before it becomes a system-wide problem, and a good extra defense in case some unreasonably large allocation sneaks past application-specific limits.
These "impossible" things happen regularly in the services I worked on. The hardest part about handling them has been Rust's libstd sabotaging it and giving up before even trying. Handling of OOM works well enough to be useful where Rust's libstd doesn't get in the way.
Rust is the problem here.
Simplest example is to allocate and pin all your resources on startup. If it crashes, it does so immediately and with a clear error message, so the solution is as straightforward as "pass bigger number to --memory flag" or "spec out larger machine".
To me, the whole point of Zig's explicit allocator dependency injection design is to make it easy to not use the system allocator, but something more effective.
For example imagine a web server where each request handler gets 1MB, and all allocations a request handler does are just simple "bump allocations" in that 1MB space.
This design has multiple benefits: - Allocations don't have to synchronize with the global allocator. - Avoids heap fragmentation. - No need to deallocate anything, we can just reuse that space for the next request. - No need to care about ownership -- every object created in the request handler lives only until the handler returns. - Makes it easy to define an upper bound on memory use and very easy to detect and return an error when it is reached.
In a system like this, you will definitely see allocations fail.
And if overcommit bothers someone, they can allocate all the space they need at startup and call mlock() on it to keep it in memory.
ever? If you have limited RAM and limited storage on a small linux SBC, where does it put your memory?
How does that work in the presence of recursion or calls through function pointers?
Function pointers: Zig has a proposal for restricted function types [1], which can be used to enforce compile-time constraints on the functions that can be assigned to a function pointer.
[0]: https://github.com/ziglang/zig/issues/1006 [1]: https://github.com/ziglang/zig/issues/23367
Certainly I agree that allocations in your dependencies (including std) are more annoying in Rust since it uses panics for OOM.
The no-std set of crates is all setup to support embedded development.
Also malloc can fail even with overcommit, if you accidentally enter an obviously incorrect size like -1.
But you only need about 5% of the concepts in that comment to be productive in Rust. I don't think I've ever needed to know about #[fundamental] in about 12 years or so of Rust…
> In both Go and Rust, allocating an object on the heap is as easy as returning a pointer to a struct from a function. The allocation is implicit. In Zig, you allocate every byte yourself, explicitly. […] you have to call alloc() on a specific kind of allocator,
> In Go and Rust and so many other languages, you tend to allocate little bits of memory at a time for each object in your object graph. Your program has thousands of little hidden malloc()s and free()s, and therefore thousands of different lifetimes.
Rust can also do arena allocations, and there is an allocator concept in Rust, too. There's just a default allocator, too.
And usually a heap allocation is explicit, such as with Box::new, but that of course might be wrapped behind some other type or function. (E.g., String, Vec both alloc, too.)
> In Rust, creating a mutable global variable is so hard that there are long forum discussions on how to do it.
The linked thread is specifically about creating a specific kind of mutable global, and has extra, special requirements unique to the thread. The stock "I need a global" for what I'd call a "default situation" can be as "simple" as,
static FOO: Mutex<T> = Mutex::new(…);
Since mutable globals are inherently memory unsafe, you need the mutex.(Obviously, there's usually an XY problem in such questions, too, when someone wants a global…)
To the safety stuff, I'd add that Rust not only champions memory safety, but the type system is such that I can use it to add safety guarantees to the code I write. E.g., String can guarantee that it always represents a Unicode string, and it doesn't really need special support from the language to do that.
The similar argument against C++ is applicable here: another programmer may be using 10% (or a different 5%) of the concepts. You will have to learn that fraction when working with him/her. This may also happen when you read the source code of some random projects. C programmers seldom have this problem. Complexity matters.
Before LLMs, only the author had a firm grasp of how their convoluted solution works.
Now sometimes not even the author knows wtf is going on among thousands of added lines of code.
But, for loops get tedious. So people will make helper functions. Generic ones today, non-generic in the past. The result is that you have a zoo of iteration-related helper functions all throughout. You'll need to learn those when onboarding to a new code base as well. Go's readability makes this easier, but by definitions everything's entirely non-standard.
This is overblown, imo. Now just generics exist, you just define Map(), Filter(), and Reduce() in your internal util package. So yes, a new dev needs to find the util package. But they need to do that anyway.
What’s more, these particular functions don’t spread into the type signatures of other functions. That means a new dev only has to go looking for them when they themselves want to use those functions.
Sure, it’s not entirely ideal maybe. But the tone and content of your comment makes it sound a zillion times worse than it is.
> Rust can also do arena allocations, and there is an allocator concept in Rust, too. There's just a default allocator, too.
Thank you. I've seen this repeated so many times. Casey Muratori did a video on batch allocations that was extremely informative, but also stupidly gatekeepy [1]. I think a lot of people who want to see themselves as super devs have latched onto this point without even understanding it. They talk like RAII makes it impossible to batch anything.
Last year the Zig Software Foundation wrote about Asahi Lina's comments around Rust and basically implied she was unknowingly introducing these hidden allocations, citing this exact Casey Muratori video. And it was weird. A bunch of people pointed out the inaccuracies in the post, including Lina [2]. That combined with Andrew saying Go is for people without taste (not that I like Go myself), I'm not digging Zig's vibe of dunking on other companies and languages to sell their own.
[1] https://www.youtube.com/watch?v=xt1KNDmOYqA [2] https://lobste.rs/s/hxerht/raii_rust_linux_drama
Though I'd still reach for something like Bumpalo ( https://crates.io/crates/bumpalo ) unless I had good reason to avoid it.
Is there a language that can't?
The author isn't saying it's literally impossible to batch allocate, just that the default happy path of programming in Rust & Go tends to produce a lot of allocations. It's a take more nuanced than the binary possible vs impossible.
https://docs.oracle.com/en/java/javase/25/docs/api/java.base...
aren't allocators types in rust?
suppose you had an m:n system (like say an evented http request server split over several threads so that a thread might handle several inbound requests), would you be able to give each request its own arena?
And so if in your example every request can have the same Allocator type, and then have distinct instances of that type . For example, you could say "I want an Arena" and pick the Arena type that impls Allocator, and then create a new instance of Arena for each `Vec::new_in(alloc)` call.
Alternately, if you want every request to have a distinct Allocator type as well as instance, one can use `Box<dyn Allocator>` as the allocators type (or use any other dispatch pattern), and provide whatever instance of the allocator is appropriate.
Just a pure question: Is Rust allocator global? (Will all heap allocations use the same allocator?)
The standard library provides a global allocator. The collections in the standard library currently use that allocator.
It also provides an unstable interface for allocators in general. That's of course useful someday, but also doesn't prevent people from using whatever allocators they want in the meantime. It just means that libraries that want to be generic over one cannot currently agree. The standard library collections also will use that once it becomes stable.
> The idea seems to be that you can run your program enough times in the checked release modes to have reasonable confidence that there will be no illegal behavior in the unchecked build of your program. That seems like a highly pragmatic design to me.
This is only pragmatic if you ignore the real world experience of sanitizers which attempt to do the same thing and failing to prevent memory safety and UB issues in deployed C/C++ codebases (eg Android definitely has sanitizers running on every commit and yet it wasn’t until they switched to Rust that exploits started disappearing).
https://security.googleblog.com/2025/11/rust-in-android-move...
Well, not exactly. This is actually a great example of the Go philosophy of being "simple" while not being "easy".
A Vec<T> has identity; the memory underlying a Go slice does not. When you call append(), a new slice is returned that may or may not share memory with the old slice. There's also no way to shrink the memory underlying a slice. So slices actually very much do not work like Vec<T>. It's a common newbie mistake to think they do work like that, and write "append(s, ...)" instead of "s = append(s, ...)". It might even randomly work a lot of the time.
Go programmer attitude is "do what I said, and trust that I read the library docs before I said it". Rust programmer attitude is "check that I did what I said I would do, and that what I said aligns with how that library said it should be used".
So (generalizing) Go won't implement a feature that makes mistakes harder, if it makes the language more complicated; Rust will make the language more complicated to eliminate more mistakes.
Sorry, that is incorrect: https://pkg.go.dev/slices#Clip
> It's a common newbie mistake to think they do work like that, and write "append(s, ...)" instead of "s = append(s, ...)". It might even randomly work a lot of the time.
"append(s, ...)" without the assignment doesn't even compile. So your entire post seems like a strawman?
https://go.dev/play/p/icdOMl8A9ja
> So (generalizing) Go won't implement a feature that makes mistakes harder, if it makes the language more complicated
No, I think it is more that the compromise of complicating the language that is always made when adding features is carefully weighed in Go. Less so in other languages.
Clipping doesn't seem to automatically move the data, so while it does mean appending will reallocate, it doesn't actually shrink the underlying array, right?
b := append(a, …)I agree and think Go gets unjustly blamed for some things: most of the foot guns people say Go has are clearly laid out in the spec/documentation. Are these surprising behaviors or did you just not read?
Getting a compiler and just typing away is not a great way of going about learning things if that compiler is not as strict.
For Go, I wouldn't say that the choice to avoid generics was either intentional or minimalist by nature. From what I recall, they were just struggling for a long time with a difficult decision, which trade-offs to make. And I think they were just hoping that, given enough time, the community could perhaps come up with a new, innovative solution that resolves them gracefully. And I think after a decade they just kind of settled on a solution, as the clock was ticking. I could be wrong.
For Rust, I would strongly disagree on two points. First, lifetimes are in fact what tripped me up the most, and many others, famously including Brian Kernighan, who literally wrote the book on C. Second, Rust isn't novel in combining many other ideas into the language. Lots of languages do that, like C#. But I do recall thinking that Rust had some odd name choices for some features it adopted. And, not being a C++ person myself, it has solutions to many problems I never wrestled with, known by name to C++ devs but foreign to me.
For Zig's manual memory management, you say:
> this is a design choice very much related to the choice to exclude OOP features.
Maybe, but I think it's more based on Andrew's need for Data-Oriented Design when designing high performance applications. He did a very interesting talk on DOD last year[1]. I think his idea is that, if you're going to write the highest performance code possible, while still having an ergonomic language, you need to prioritize a whole different set of features.
Indeed, in 2009 Russ Cox laid out clearly the problem they had [1], summed up thus:
> The generic dilemma is this: do you want slow programmers, slow compilers and bloated binaries, or slow execution times?
My understanding is that they were eventually able to come up with something clever under the hood to mitigate that dilemma to their satisfaction.
I am sad that it does not mention Raku (https://raku.org) ... because in my mind there is a kind of continuum: C - Zig - C++ - Rust - Go ... OK for low level, but what about the scriptier end - Julia - R - Python - Lua - JavaScript - PHP - Raku - WL?
Raku
Raku stands out as a fast way to working code, with a permissive compiler that allows wide expression.
Its an expressive, general-purpose language with a wide set of built-in tools. Features like multi-dispatch, roles, gradual typing, lazy evaluation, and a strong regex and grammar system are part of its core design. The language aims to give you direct ways to reflect the structure of a problem instead of building abstractions from scratch.
The grammar system is the clearest example. Many languages treat parsing as a specialized task requiring external libraries. Raku instead provides a declarative syntax for defining rules and grammars, so working with text formats, logs, or DSLs often requires less code and fewer workarounds. This capability blends naturally with the rest of the language rather than feeling like a separate domain.
Raku programs run on a sizeable VM and lean on runtime dispatch, which means they typically don’t have the startup speed or predictable performance profile of lower-level or more static languages. But the model is consistent: you get flexibility, clear semantics, and room to adjust your approach as a problem evolves. Incremental development tends to feel natural, whether you’re sketching an idea or tightening up a script that’s grown into something larger.
The language’s long development history stems from an attempt to rethink Perl, not simply modernize it. That history produced a language that tries to be coherent and pleasant to write, even if it’s not small. Choose Raku if you want a language that let's you code the way you want, helps you wrestle with the problem and not with the compiler.
Some comments below on “I want a Go, but with more powerful OO” - well Raku adheres to the Smalltalk philosophy… everything is an object, and it has all the OO richness (rope) of C++ with multiple inheritance, role composition, parametric roles, MOP, mixins… all within an easy to use, easy to read style.
my $forty-two = 42 but 'forty two';
Look away now if you hate sigils.Between the lack of "colored functions" and the simplicity of communicating with channels, I keep surprising myself with how (relatively) quick and easy it is to develop concurrent systems with correct behavior in Go.
Consider a server handling transactional requests, which submit jobs and get results from various background workers, which broadcast change events to remote observers.
This is straightforward to set up with channels in Go. But I haven't seen an example of this type of workload using structured concurrency.
Channels communicating between persistent workers are fine when you need decoupled asynchronous operation like that. However, channels and detached coroutines are less appropriate in a bunch of other situations, like fork-join, data parallelism, cancellation of task trees, etc. You can still do it, but you're responsible for adding that structure, and ensuring you don't forget to wait for something, don't forget to cancel something.
Erlang programmers might disagree with you there.
But distributed systems are hard. If your system isn't inherently distributed, then don't rush towards a model of concurrency that emulates a distributed system. For anything on a single machine, prefer structured concurrency.
Really the only thing I found difficult is finding the concrete implementation of an interface when the interface is defined close to where it is, and when interfaces are duplicated everywhere.
The options I’ve seen so far are: OCaml, D, Swift, Nim, Crystal, but none of them have seen to be able to capture a significant market.
Also, at least at the time, the community was really hostile, but that was true of C++, Ada, and Java communities as well well. But I think those guys have chilled out, so maybe OCaml has too?
* thiserror: I spend ridiculous and unpredictable amounts of time debugging macro expansions
* manually implementing `Error`, `From`, etc traits: I spend ridiculous though predictable amounts of time implementing traits (maybe LLMs fix this?)
* anyhow: this gets things done, but I'm told not to expose these errors in my public API
Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
And when I ask these questions to various Rust people, I often get conflicting answers and no one seems to be able to speak with the authority of canon on the subject. Maybe some of these questions have been answered in the Rust Book since I last read it?
By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.
What `thiserror` or manually implementing `Error` buys you is the ability to actually do something about higher-level errors. In Rust design, not doing so in a public facing API is indeed considered bad practice. In Go, nobody seems to care about that, which of course makes code easier to write, but catching errors quickly becomes stringly typed. Yes, it's possible to do it correctly in Go, but it's ridiculously complicated, and I don't think I've ever seen any third-party library do it correctly.
That being said, I agree that manually implementing `Error` in Rust is way too time-consuming. There's also the added complexity of having to use a third-party crate to do what feels like basic functionality of error-handling. I haven't encountered problems with `thiserror` yet.
> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
If you wish to make sure it's not a breaking change, mark your enum as `#[non_exhaustive]`. Not terribly elegant, but that's exactly what this is for.
Hope it helped a bit :)
Is it a new error condition that downstream consumers want to know about so they can have different logic? Add the enum variant. The entire point of this pattern is to do what typed exceptions in Java were supposed to do, give consuming code the ability to reason about what errors to expect, and handle them appropriately if possible.
If your consumer can't be reasonably expected to recover? Use a generic failure variant, bonus points if you stuff the inner error in and implement std::Error so consumers can get the underlying error by calling .source() for debugging at least.
> By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.
Nothing stopping you from doing the same in Rust, just add a match arm with a wildcard pattern (_) to handle everything but your special cases.
In fact, if you suspect you are likely to add additional error variants, the `#[non_exhaustive]` attribute exists explicitly to handle this. It will force consumers to provide a match arm with a wildcard pattern to prevent additions to the enum from causing API incompatibility. This does come with some other limitations, so RTFM on those, but it does allow you to add new variants to an Error enum without requiring a major semver bump.
However, I wouldn't recommend it. Breakage over errors is not necessarily a bad thing. If you need to change the API for your errors, and downstreams are required to have generic cases, they will be forced to silently accept new error types without at least checking what those new error types are for. This is disadvantageous in a number of significant cases.
You can annotate your error enum with #[non_exhaustive], then it will not be a breaking change if you add a new variant. Effectively, you enforce that anybody doing a match on the enum must implement the "default" case, i.e. that nothing matches.
My hope is they will see these repeated pain points and find something that fits the error/result/enum issues people have. (Generics will be harder, I think)
I see the desire to avoid mucking with control flow so much but something about check/handle just seemed so elegant to me in semi-complex error flows. I might be the only one who would have preferred that over accepting generics.
I can't remember at this point because there were so many similar proposals but I think there was a further iteration of check/handle that I liked better possibly but i'm obviously not invested anymore.
I kinda got used to it eventually, but I'll never ever consider not having enums a good thing.
Though I think it's more of a hobby language. The last commit was > 1 year ago.
Meanwhile Go's is just multiple value-returns with no checks whatsoever and you can return both a valid value and an error.
I can't figure out what the author is envisioning here for Rust.
Maybe, they actually think if they make a pointer to some local variable and then return the pointer, that's somehow allocating heap? It isn't, that local variable was on the stack and so when you return it's gone, invalidating your pointer - but Rust is OK with the existence of invalid pointers, after all safe Rust can't dereference any pointers, and unsafe Rust declares the programmer has taken care to ensure any pointers being dereferenced are valid (which this pointer to a long dead variable is not)
[If you run a new enough Rust I believe Clippy now warns that this is a bad idea, because it's not illegal to do this, but it's almost certainly not what you actually meant]
Or maybe in their mind, Box<Goose> is "a pointer to a struct" and so somehow a function call Box::new(some_goose) is "implicit" allocation, whereas the function they called in Zig to allocate memory for a Goose was explicit ?
Go isn't like C in that you can actually fit the entire language in your head. Most of us who think we have fit C in our head will still stumble on endless cases where we didn't realize X was actually UB or whatever. I wonder how much C's reputation for simplicity is an artifact of its long proximity to C++?
Give an example of UB code that you have committed in real life, not from blogs. I am genuinely curious.
struct foo {
...
atomic_int v;
...
};
struct foo x;
memset(&x, 0, sizeof(x));the most odd one probably being 'const expected = [_]u32{ 123, 67, 89, 99 };'
and the 2nd most being the word 'try' instead of just ?
the 3rd one would be the imports
and `try std.fs.File.stdout().writeAll("hello world!\n");` is not really convincing either for a basic print.
For example, Pythons syntax is quite nice for the most part, but I hate indentation being syntax. I like braces for scoping, I just do. Rust exists in both camps for me; I love matching with Result and Option, but lifetime syntax confuses me sometimes. Not everyone will agree, they are opinions.
No, this is a wild claim that shows you've either never written async Rust or never written heavily templated C++. Feel free to give code examples if you want to suggest otherwise.
All control flow in Zig is done via keyword
constant array with u32, and let the compiler figure out how many of em there are (i reserve the right to change it in the future)
Mutable globals are easy in Zig (presented as freedom, not as "you can now write data races.")
Runtime checks you disable in release builds are "highly pragmatic," with no mention of what happens when illegal behavior only manifests in production.
The standard library having "almost zero documentation" is mentioned but not weighted as a cost the way Go's boilerplate or Rust's learning curve are.
The RAII critique is interesting but also somewhat unfair because Rust has arena allocators too, and nothing forces fine-grained allocation. The difference is that Rust makes the safe path easy and the unsafe path explicit whereas Zig trusts you to know what you're doing. That's a legitimate design, hacking-a!
The article frames Rust's guardrails as bureaucratic overhead while framing Zig's lack of them as liberation, which is grading on a curve. If we're cataloging trade-offs honestly
> you control the universe and nobody can tell you what to do
...that cuts both ways...
At first glance you can just use static variable of a type supporting interior mutability - RefCell, Mutex, etc…
They're not.
fn main() {
unsafe {
COUNTER += 1;
println!("COUNTER = {}", COUNTER);
}
unsafe {
COUNTER += 10;
println!("COUNTER = {}", COUNTER);
}
}
Global mutable variables are as easy in Rust as in any other language. Unlike other languages, Rust also provides better things that you can use instead.And that’s where a number of people blow a gasket.
In a sense, it is a powerful kind of freedom to choose a language that protects us from the statistically likely blunders. I prefer a higher-level kind of freedom -- one that provides peace of mind from various safety properties.
This comment is philosophical -- interpret and apply it as you see fit -- it is not intended be interpreted as saying my personal failure modes are the same as yours. (e.g. Maybe you don't mind null pointer exceptions in the grand scheme of things.)
Random anecdote: I still have a fond memory of a glorious realization in Haskell after a colleague told me "if you design your data types right, the program just falls into place".
There's a similar quote from The Mythical Man Month [0, page 102]:
> Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they’ll be obvious.
And a somewhat related one from Linus [1]:
> I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships.
[0]: https://www.cs.cmu.edu/afs/cs/academic/class/15712-s19/www/p...
A programming language forces a culture on everybody in the project - it's not just a personal decision like your example.
This is perhaps somewhat natural; people like and want to be good at things. Where you fall on the trade off is up to you.
And the compiler had nothing to say about it. "Carry on, thisi is perfectly fine rust code that might crash your app with a panic if left unchecked, no biggie. LGTM" - rust compiler
Odin vs Rust vs Zig would be more apt, or Go vs Java vs OCaml or something...
I love this. I'm gonna steal this :)
> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust:
The overall point here is fair, but I think it's important to clarify that (iiuc) this comment is talking about a "soundness hole". Soundness holes are cases where there's a bug in the compiler or in a library that allows someone to commit UB without writing `unsafe`. Given the goals of Rust, it doesn't matter how ungodly complicated and contrived the example is. If it produces UB without `unsafe`, then it's a bug that needs to be fixed. In practice, that means a lot of issue threads about soundness involve mind-numbing code samples that mash different features together in unintuitive ways.
But that's a good thing! No one's saying you'll ever need to look at code like this in the wild. They're saying that no matter how hard you (or your coworkers or your dependencies) try, Rust should never fail to protect memory safety in safe code.
Thus not a general article. For some criteria Python will be a good Rust alternative.
>Can I have a #programming language/compiler similar to #Rust, but with less syntactic complexity?
That's a good question. But considering Zig is manually memory managed and Crystal/Go are garbage collected, you sidestep Rust's strongest selling point.
This is exactly why I find Go to be an excellent language. Most of the times, Go is the right tool.
Rust doesn't feel like a tool. Ceremonial yet safe and performant.
Sure, you can fit all of C in your head, including all the obscure footguns that can lead to UB: https://gist.github.com/Earnestly/7c903f481ff9d29a3dd1
And other fun things like aliasing rules and type punning.
Out of all languages I do development in the past few months: Go, Rust, Python, Typescript; Rust is the one that LLM has the least churn/problems in terms of producing correct and functional code given a problem of similar complexity.
I think this outside factor will eventually win more usage for Rust.
Like rust seems particularly well suited for an agent based workflow, in that in theory an agent with a task could keep `cargo check`-ing it's solutions, maybe pulling from docs.rs or source for imported modules, and get to a solution that works with some confidence (assuming the requirements were well defined/possible etc etc).
I've had a mixed bag of an experience trying this with various rust one off projects. It's definitely gotten me some prototype things working, but the evolving development of rust and crates in the ecosystem means there's always some patchwork to get things to actually compile. Anecdotally I've found that once I learned more about the problem/library/project I'll end up scrapping or rewriting a lot of the LLM code. It seems pretty hard to tailor/sandbox the context and workflow of an agent to the extent that's needed.
I think the Bun acquisition by Anthropic could shift things too. Wouldn't be surprised if the majority of code generated/requested by users of LLM's is JS/TS, and Anthropic potentially being able to push for agentic integration with the Bun runtime itself could be a huge boon for Bun, and maybe Zig (which Bun is written in) as a result? Like it'd be one thing for an agent to run cargo check, it'd be another for the agent to monitor garbage collection/memory use while code is running to diagnose potential problems/improvements devs might not even notice until later. I feel like I know a lot of devs who would never touch any of the langs in this article (thinking about memory? too scary!) and would love to continue writing JS code until they die lol
- Can't `for (-1..1) {`. Must use `while` instead.
- if you allocated something inside of a block and you want it to keep existing outside of a block `defer` won't help you to deallocate it. I didn't find a way to defer something till the end of the function.
- adding variable containing -1 to usize variable is cumbersome. You are better of running everything with isize and converting to usize as last operation wherever you need it.
- language evolved a bunch and LLMs are of very little help.
Deallocating the wrong thing or the right thing too soon bit me in th ass so much already that I feel craving for destructors.
Which can be a worthwhile cost if the benefits of speed and security are needed. But I think it's certainly a cognitive cost.
1) Complementary tools. I picked python and rust for obvious reasons given their differences
2) Longevity. Rust in kernel was important to me because it signaled this isn’t going anywhere. Same for rust invading the tool stacks of various other languages and the rewrite everything in rust. I know it irritates people but for me it’s a positive signal on it being worth investing time into
It was fun to read, but I don't see anything new here, and I don't agree too much.
https://github.com/rust-lang/rust/issues/68015#issuecomment-...
Wow, Rust does take programming complexity to another level.
Everything, including programming languages, need to be simple but no simpler. I'm of the opinion that most the computing and memory resources complexity should be handled and abstracted by the OS for example the address space isolation [1].
The author should try D language where it's the Goldilocks of complexity and meta programming compared to Go, Rust and Zig [2].
[1] Linux address space isolation revived after lowering performance hit (59 comments):
https://news.ycombinator.com/item?id=44899488
[2] Ask HN: Why do you use Rust, when D is available? (255 comments):
> What is the dreaded UB? I think the best way to understand it is to remember that, for any running program, there are FATES WORSE THAN DEATH. If something goes wrong in your program, immediate termination is great actually!
This has nothing to do with UB. UB is what it says on the tin, it's something for which no definition is given in the execution semantics of the language, whether intentionally or unintentionally. It's basically saying, "if this happens, who knows". Here's an example in C:
int x = 555;
long long *l = (long long*)&x;
x = 123;
printf("%d\n", *l);
This is a violation of the strict aliasing rule, which is undefined behavior. Unless it's compiled with no optimizations, or -fno-strict-aliasing which effectively disables this rule, the compiler is "free to do whatever it wants". Effectively though, it'll just print out 555 instead of 123. All undefined behavior is just stuff like this. The compiler output deviates from the expected input, and only maybe. You can imagine this kind of thing gets rather tricky with more aggressive optimizations, but this potential deviation is all that occurs.Race conditions, silent bugs, etc. can occur as the result of the compiler mangling your code thanks to UB, but so can crashes and a myriad of other things. It's also possible UB is completely harmless, or even beneficial. It's really hard to reason about that kind of thing though. Optimizing compilers can be really hard to predict across a huge codebase, especially if you aren't a compiler dev yourself. That unpredictability is why we say it's bad. If you're compiling code with something like TCC instead of clang, it's a completely different story.
That's it. That's all there is to UB.
You don’t think that’s pretty bad?
Interestingly enough, and only semi related, I had to use volatile for the first time ever in my latest project. Mainly because I was writing assembly that accessed memory directly, and I wanted to make sure the compiler didn't optimize away the variable. I think that's maybe the last C keyword on my bucket list.
People are taught it’s very bad because otherwise they do exactly this, which is the problem. What does your compiler do here may change from invocation to invocation, due to seemingly unrelated flags, small perturbations in unrelated code, or many other things. This approach encourages accepting UB in your program. Code that invokes UB is incorrect, full stop.
Careful. It's not just "consult your compiler", because the behavior of a given compiler on code containing UB is also allowed to vary based on specific compiler version, and OS, and hardware, and the phase of the moon.
> It seems the Go development team has a high bar for adding features to the language. The end result is a language that forces you to write a lot of boilerplate code to implement logic that could be more succinctly expressed in another language.
Being able to implement logic more succinctly is not always a good thing. Take error handling syntactic sugar for example. Consider these two snippets:
let mut file = File::create("foo.txt")?;
and: f, err := os.Create("filename.txt")
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
The first code is more succinct, but worse: there is no context added to the error (good luck debugging!).Sometimes, being forced to write code in a verbose manner makes your code better.
If you want to add 'proper' error types, wrapping them is just as difficult in Go and Rust (needing to implement `Error` in Go or `std::Error` in Rust). And, while we can argue about macro magic all day, the `thiserror` crate makes said boilerplate a non-issue and allows you to properly propagate strongly-typed errors with context when needed (and if you're not writing library code to be consumed by others, `anyhow` helps a lot too).
The proof is in the pudding, though. In my experience, working across Go codebases in open source and in multiple closed-source organizations, errors are nearly universally wrapped and handled appropriately. The same is not true of Rust, where in my experience ? (and indeed even unwrap) reign supreme.
f = open('foo.txt', 'w')
is even more succinct, and the exception thrown on failure will not only contain the reason, but the filename and the whole backtrace to the line where the error occurred. try:
f = open('foo.txt', 'w')
except Exception as e:
raise NecessaryContext("important information") from e
Else your callers are in for a nightmare of a time trying to figure out why an exception was thrown and what to do with it. Worse, you risk leaking implementation details that the caller comes to depend on which will also make your own life miserable in the future.That simple example in Python is missing all the other stuff you have to put around it. Go would have another error check, but I get to decide, at that point in the execution, how I want to handle it in this context
... with no other context whatsoever, so you can't glean any information about the call stack that led to the exception.
Exceptions are really a whole different kettle of fish (and in my opinion are just strictly worse than even the worst errors-as-values implementations).
But in go you can just _err and never touch it.
Also while not part of std::Result you can use things like anyhow or error_context to add context before returning if theres an error.
You can do that in Rust too. This code doesn't warn:
let _ = File::create("foo.txt");
(though if you want code that uses the File struct returned from the happy path of File::create, you can't do that without writing code that deals somehow with the possibility of the create() call failing, whether it is a panic, propagating the error upwards, or actual error handling code. Still, if you're just calling create() for side effects, ignoring the error is this easy.)Rust used to not have operator?, and then A LOT of complaints have been "we don't care, just let us pass errors up quickly"
"good luck debugging" just as easily happens simply by "if err!=nil return nil,err" boilerplate that's everywhere in Golang - but now it's annoying and takes up viewspace
This isn't true in my experience. Most Go codebases I've worked in wrap their errors.
If you don't believe me, go and take a look at some open-source Go projects.
do {
let file = try FileManager.create(…)
} catch {
logger.error("Failed creating file", metadata: ["error": "\(error)"])
}
Note the try is not actual CPU exceptions, but mostly syntax sugar.You can opt-out of the error handling, but it’s frowned upon, and explicit:
let file = try? FileManager.create(…)
or let file = try! FileManager.create(…)
The former returning an optional file if there is an error, and the latter crashing in case of an error.In Rust I could have done (assuming `anyhow::Error` or `Box<dyn Error + Send + Sync>` return types, which are very typical):
let mut file = File::create("foo.txt")
.map_err(|e| format!("failed to create file: {e}")?;
Rust having the subtle benefit here of guaranteeing at compile type that the parameter to the string is not omitted.In Go I could have done (and is just as typical to do):
f, err := os.Create("filename.txt")
if err != nil {
return err
}
So Go no more forces you to do that than Rust does, and both can do the same thing. ?
is too strong.The UX is terrible — the path of least resistance is that of laziness. You should be forced to provide an error message, i.e.
?("failed to create file: {e}")
should be the only valid form.In Go, for one reason or another, it's standard to provide error context; it's not typical at all to just return a bare `err` — it's frowned upon and unidiomatic.
Go's wrapping of errors is just a crappy exception stack trace with less information.
You are also not forced to add context. Hell, you can easily leave errors unhandled, without compiler errors nor warnings, which even linters won't pick up, due to the asinine variable syntax rules.
It's quite ridiculous that you're claiming errors can be easily left unhandled while referring to what, a single unfortunate pattern of code that will only realistically happen due to copy-pasting and gets you code that looks obviously wrong? Sigh.
This really isn't an issue in practice. The only case where an error wouldn't uniquely identify its call stack is if you were to use the exact same context string within the same function (and also your callees did the same). I've never encountered such a case.
> You are also not forced to add context
Yes, but in my experience Go devs do. Probably because they're having to go to the effort of typing `if err != nil` anyway, and frankly Go code with bare:
if err != nil {
return err
}
sticks out like a sore thumb to any experienced Go dev.> which even linters won't pick up, due to asinine variable syntax rules.
I have never encountered a case where errcheck failed to detect an unhandled error, but I'd be curious to hear an example.
You can write your own allocator in C. You don't have to use malloc.
I actually love how rust gatekeeps the idiots from programming it, probably why Linus Torvalds allowed rust into the kernel, but not C++.
Eh, that's not typical Rust project code though. It is Rust code inside the std lib. std libs of most languages including Python are a masterclass in dark arts. Rust is no exception.
The human brain demands "vs" articles
- has the ergonomics, abstractions, expressiveness and conveniences of a high-level language with pointer-level semantics if/when needed (essentially covering the abstraction vs. cost spectrum of all those languages)
- can be used with or without GC (same)
- has the libraries and tooling to tackle massively concurrent/parallel workloads with ease (the niche that Go carved for itself)
- offers the same memory safety guarantees as Rust, and possibly more in the future with capture-checking (a more general concept than borrow-checking to guarantee resources scoping at compile-time)
I feel like Zig is for the C / C++ developers that really dislike Rust.
There have been other efforts like Carbon, but this is the first that really modernizes the language and scratches new itches.
> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust: [crazy example elided]
That is totally unfair. 99% of your time with Rust won't be anything like that.
> This makes Rust hard, because you can’t just do the thing! You have to find out Rust’s name for the thing—find the trait or whatever you need—then implement it as Rust expects you to.
What?
Rust is not hard. Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.
If you're trying to shoehorn some novel type of yours into a particular trait interface so you can pass trait objects around, sure. Maybe you are going to have to memorize a lot more. But I'd ask why you write code like that unless you're writing a library.
This desire of wanting to write OO-style code makes me think that people who want OO-style code are the ones having a lot of struggle or frustration with Rust's ergonomics.
Rust gives you everything OO you'd want, but it's definitely more favorable if you're using it in a functional manner.
> makes consuming libraries easy in Rust and explains why Rust projects have almost as many dependencies as projects in the JavaScript ecosystem.
This is one of Rust's superpowers !
I would read this in regard to Go and not so much in regards to Zig. Go is insanely productive, and while you're not going to match something like Django in terms of delivery speed with anything in Go, you almost can... and you can do it without using a single external dependency. Go loses a little of this in the embeded space, where it's not quite as simple, but the opinonated approach is still very productive even here.
I can't think of any language where I can produce something as quickly as I can in Go with the use of nothing but the standard library. Even when you do reach for a framework like SQLC, you can run the external parts in total isolation if that's your thing.
I will say that working with the interoperability of Zig in our C for Python binaries has been very easy, which it wasn't for Rust. This doesn't mean it's actually easier for other people, but it sure was for me.
> This is one of Rust's superpowers !
In some industries it's really not.
I find Rust quite easy most of the time. I enjoy the hell out of it and generally write Rust not too different than i'd have written my Go programs (i use less channels in Rust though). But i do think my comment about rope is true. Some people just can't seem to help themselves.
Rust OTOH is obsessively precise about enforcing these sort of things.
Of course Rust has a lot of features and compiles slower.
Perhaps it is because DDD books and the like usually have strong object oriented biases, but whenever I read about functional programming patterns I’m never clear on how to go from exercise stuff to something that can work in a real world monolith for example.
And to be clear I’m not saying functional programming is worse at that, simply that I have not been able to find information on the subject as easily.
Here is one about how to structure a project (roughly)
https://youtube.com/watch?v=XpDsk374LDE
I also think looking at the source code for elm and its website, as well as the elm real world example help a lot.
Also my feeling. Writing this as a former C++ developer who really likes Rust :)
I believe this is actually a significant source of consternation. I teach Rust to students, and I find those without a C/C++ background take to it more naturally than those who have a lot of experience with those languages. People with a C/C++ background approach Rust like C/C++ and fight it the whole way, whereas people without that background approach Rust as Rust, and they have a better time with it.
> If you're trying to shoehorn some novel type of yours into a particular trait interface so you can pass trait objects around, sure. Maybe you are going to have to memorize a lot more. But I'd ask why you write code like that unless you're writing a library.
I think that you are missing the point - they're not saying (at least in my head) "Rust is hard because of all the abstractions" but, more "Rust is hard because you are having to explain to the COMPILER [more explicitly] what you mean (via all these abstractions)
And I think that that's a valid assessment (hell, most Rustaceans will point to this as a feature, not a bug)
Can you elaborate? While they obviously have overlap, Rust's stdlib is deliberately minimal (you don't even get RNG without hitting crates.io), whereas Python's is gigantic. And in actual use, they tend to feel extremely different.
If you know Java, you can read C#, JavaScript, Dart, and Haxe and know what's going on. You can probably figure out Go.
Rust is like learning how to program again.
Back when I was young and tried C++, I was like this is hard and I can't do this.
Then I found JavaScript and everything was great.
What I really want is JS that complies into small binaries and runs faster than C. Maybe clean up the npm dependency tree. Have a professional commite vet every package.
I don't think that's possible, but I can dream
use the stuff you want people.
I'm a bit of a Rust fanboy because of writing so much Go and Javascript in the past. I think I just got tired of all the footguns and oddities people constantly run into but conveniently brush off as intentional by the design of the language. Even after years of writing both, I would still get snagged on Go's sharp edges. I have seen so many bugs with Go, written by seniors, because doing the thing seemed easy in code only for it to have unexpected behavior. This is where even after years of enjoying Go, I have a bit of a bone to pick with it. Go was designed to be this way (where Javascript/Typescript is attempting to make up for old mistakes). I started to think to myself: Well, maybe this shouldn't be "easy" because what I am trying to do is actually complicated behind the scenes.
I am not going to sit here and argue with people around language design or computer science. What I will say is that since I've been forced to be somewhat competent in Rust, I am a far better programmer because I have been forced to grasp concepts on a lower level than before. Some say this might not be necessary or I should have known these things before learning Rust, and I would agree, but it does change the way you write and design your programs. Rust is just as ugly and has snags that are frustrating like any other language, yes, but it was the first that forced me to really think about what it is I am trying to do when writing something that the compiler claims is a no-no. This is why I like Zig as well and the syntax alone makes me feel like there is space for both.
> OOP has been out of favor for a while now
I love these lines. Who writes this stuff? I'll tell you: The same people on HN who write "In Europe, X is true." (... when Europe is 50 countries!). > Zig is a language for data-oriented design.
But not OOP, right? Or, OOP couldn't do the same thing?One thing that I have found over umpteen years of reading posts online: Americans just love superlatives. They love the grand, sweeping gesture. Read their newspapers; you see it every day. A smidge more minimalism would make their writing so much more convincing.
I will take some downvotes for this ad hominem attack: Why does this guy have 387 connections on LinkedIn? That is clicking the "accept" button 387 times. Think about that.
Rust for WASM
Zig is what I'd use if I started a greenfield DBMS project
There are bad cases of RAII APIs for sure, but it's not all bad. Andrew posted himself a while back about feeling bad for go devs who never get to debug by seeing 0xaa memory segments, and sure I get it, but you can't make over-extended claims about non-initialization when you're implicitly initializing with the magic value, that's a bit of a false equivalence - and sure, maybe you don't always want a zero scrub instead, I'm not sold on Go's mantra of making zero values always be useful, I've seen really bad code come as a result of people doing backflips to try to make that true - a constructor API is a better pattern as soon as there's a challenge, the "rule" only fits when it's easy, don't force it.
Back to RAII though, or what people think of when they hear RAII. Scope based or automatic cleanup is good. I hate working with Go's mutex's in complex programs after spending life in the better world. People make mistakes and people get clever and the outcome is almost always bad in the long run - bugs that "should never get written/shipped" do come up, and it's awful. I think Zig's errdefer is a cool extension on the defer pattern, but defer patterns are strictly worse than scope based automation for key tasks. I do buy an argument that sometimes you want to deviate from scope based controls, and primitives offering both is reasonable, but the default case for a ton of code should be optimized for avoiding human effort and human error.
In the end I feel similarly about allocation. I appreciate Zig trying to push for a different world, and that's an extremely valuable experiment to be doing. I've fought allocation in Go programs (and Java, etc), and had fights with C++ that was "accidentally" churning too much (classic hashmap string spam, hi ninja, hi GN), but I don't feel like the right trade-off anywhere is "always do all the legwork" vs. "never do all the legwork". I wish Rust was closer to the optimal path, and it's decently ergonomic a lot of the time, but when you really want control I sometimes want something more like Zig. When I spend too much time in Zig I get a bit bored of the ceremony too.
I feel like the next innovation we need is some sanity around the real useful value that is global and thread state. Far too much toxic hot air is spilled over these, and there are bad outcomes from mis/overuse, but innovation could spend far more time on _sanely implicit context_ that reduces programmer effort without being excessively hidden, and allowing for local specialization that is easy and obvious. I imagine it looks somewhere between the rust and zig solutions, but I don't know exactly where it should land. It's a horrible set of layer violations that the purists don't like, because we base a lot of ABI decisions on history, but I'd still like to see more work here.
So RAII isn't the big evil monster, and we need to stop talking about RAII, globals, etc, in these ways. We need to evaluate what's good, what's bad, and try out new arrangements maximize good and minimize bad.
I disagree and place RAII as the dividing line on programming language complexity and is THE "Big Evil Monster(tm)".
Once your compiled language gains RAII, a cascading and interlocking set of language features now need to accrete around it to make it ... not excruciatingly painful. This practically defines the difference between a "large" language (Rust or C++) and a "small" language (C, Zig, C3, etc.).
For me, the next programming language innovation is getting the garbage collected/managed memory languages to finally quit ceding so much of the performance programming language space to the compiled languages. A managed runtime doesn't have to be so stupidly slow. It doesn't have to be so stupidly non-deterministic. It doesn't have to have a pathetic FFI that is super complex. I see the "strong typing everywhere" as the first step along this path. Fil-C might become an interesting existence proof in this space.
I view having to pull out any of C, Zig, C++, Rust, etc. as a higher-level programming language failure. There will always be a need for something like them at the bottom, but I really want their scope to be super small. I don't want to operate at their level if I can avoid them. And I say all this as someone who has slung more than 100KLoC of Zig code lately.
For a concrete example, let's look at Ghostty which was written in Zig. There is no strong performance reason to be in Zig (except that implementations in every other programming language other than Rust seem to be so much slower). There is no strong memory reason to be in Zig (except that implementations in every other programming language other than Rust chewed up vast amounts of it). And, yet, a relatively new, unstable, low-level programming language was chosen to greenfield Ghostty. And all the other useful terminal emulators seem to be using Rust.
Every adherent of managed memory languages should take it as a personal insult that people are choosing to write modern terminal emulators in Rust and Zig.
How so? Garbage collection has inherent performance overhead wrt. manual memory management, and Rust now addresses this by providing the desired guarantees of managed memory without the overhead of GC.
A modern terminal emulator is not going to involve complex reference graphs where objects may cyclically reference one another with no clearly-defined "owner"; which is the one key scenario where GC is an actual necessity even in a low-level systems language. What do they even need GC for? Rather, they should tweak the high-level design of their program to emsure that object lifetimes are properly accounted for without that costly runtime support.
You will be very rich.