Thoughts on Go vs. Rust vs. Zig
161 points
2 hours ago
| 28 comments
| sinclairtarget.com
| HN
kibwen
1 hour ago
[-]
> In Rust, creating a mutable global variable is so hard that there are long forum discussions on how to do it. In Zig, you can just create one, no problem.

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.

reply
globalnode
1 hour ago
[-]
so does the rust compiler check for race conditions between threads at compile time? if so then i can see the allure of rust over c, some of those sync issues are devilish. and what about situations where you might have two variables closely related that need to be locked as a pair whenever accessed.
reply
tczMUFlmoNk
1 hour ago
[-]
> what about situations where you might have two variables closely related that need to be locked as a pair whenever accessed.

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.

reply
treyd
3 minutes ago
[-]
This was a primary design goal for Rust! To prevent data races (and UAF and other types of memory unsafety) by construction through the type system.
reply
andsoitis
14 minutes ago
[-]
> so does the rust compiler check for race conditions between threads at compile time?

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)

reply
throwawaymaths
12 minutes ago
[-]
and you can have good races too (where the order doesnt matter)
reply
PartiallyTyped
52 minutes ago
[-]
In rust, there are two kinds of references, exclusive (&mut) and shared(&). Rustc guarantees you that if you provide an exclusive reference, no other thread will have that. If your thread has an exclusive reference, then it can mutate the contents of the memory. Rustc also guarantees that you won't end up with a dropped reference inside of your threads, so you will always have allocated memory.

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.

https://doc.rust-lang.org/std/cell/index.html

reply
ViewTrick1002
1 hour ago
[-]
It entirely prevents race conditions due to the borrow checker and safe constructs like Mutexes.

Logical race conditions and deadlocks can still happen.

reply
kibwen
57 minutes ago
[-]
Rust's specific claims are that safe Rust is free from data races, but not free from general race conditions, including deadlocks.
reply
globalnode
1 hour ago
[-]
ah i see, thanks. i have no idea what rust code looks like but from the article it sounds like a language where you have a lot of metadata about the intended usage of a variable so the compiler can safety check. thats its trick.
reply
timschmidt
48 minutes ago
[-]
That's a fairly accurate idea of it. Some folks complain about Rust's syntax looking too complex, but I've found that the most significant differences between Rust and C/C++ syntax are all related to that metadata (variable types, return types, lifetimes) and that it's not only useful for the compiler, but helps me to understand what sort of data libraries and functions expect and return without having to read through the entire library or function to figure that out myself. Which obviously makes code reuse easier and faster. And similarly allows me to reason much more easily about my own code.
reply
mh2266
33 minutes ago
[-]
The only thing I really found weird syntactically when learning it was the single quote for lifetimes because it looks like it’s an unmatched character literal. Other than that it’s a pretty normal curly-braces language, & comes from C++, generic constraints look like plenty of other languages.

Of course the borrow checker and when you use lifetimes can be complex to learn, especially if you’re coming from GC-land, just the language syntax isn’t really that weird.

reply
timschmidt
24 minutes ago
[-]
Agreed. In practice Rust feels very much like a rationalized C++ in which 30 years of cruft have been shrugged off. The core concepts have been reduced to a minimum and reinforced. The compiler error messages are wildly better. And the tooling is helpful and starts with opinionated defaults. Which all leads to the knock-on effect of the library ecosystem feeling much more modular, interoperable, and useful.
reply
etse
55 minutes ago
[-]
That seems unusual. I would assume trivial means the default approach works for most cases. Perhaps mutable global variables are not a common use case. Unsafe might make it easier, but it’s not obvious and probably undesired. 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.
reply
kibwen
40 minutes ago
[-]
Outside of single-initialization/lazy-initialization (which are provided via safe and trivial standard library APIs: https://doc.rust-lang.org/std/sync/struct.LazyLock.html ) almost no Rust code uses global mutable variables. It's exceedingly rare to see any sort of global mutable state, and it's one of the lovely things about reading Rust code in the wild when you've spent too much of your life staring at C code whose programmers seemed to have a phobia of function arguments.
reply
stouset
32 minutes ago
[-]
The default approach is to use a container that enforces synchronization. If you need manual control, you are able to do that, you just need to explicitly opt into the responsibility that comes with it.

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.

reply
nu11ptr
40 minutes ago
[-]
> I would assume trivial means the default approach works for most cases. Perhaps mutable global variables are not a common use case. Unsafe might make it easier, but it’s not obvious and probably undesired.

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.

reply
irishcoffee
34 minutes ago
[-]
> Even if you do, you now have an effective comment that tells you where to look if you ever get suspicious behavior.

By the time suspicious behavior happens, isn’t it kind of a critical inflection point?

For example, the news about react and next that came out. Once the code is deployed, re-deploying (especially with a systems language that quite possibly lives on an air-gapped system with a lot of rigor about updates) means you might as well have used C, the dollar cost is the same.

reply
stouset
30 minutes ago
[-]
Are you with a straight face saying that occasionally having a safety bug in limited unsafe areas of Rust is functionally the same as having written the entire program in an unsafe language like C?

One, the dollar cost is not the same. The baseline floor of quality will be higher for a Rust program vs. a C program given equal development effort.

Second, the total possible footprint of entire classes of bugs is zero thanks to design features of Rust (the borrowck, sum types, data race prevention), except in a specifically delineated areas which often total zero in the vast majority of Rust programs.

reply
irishcoffee
5 minutes ago
[-]
> The baseline floor of quality will be higher for a Rust program vs. a C program given equal development effort.

Hmm, according to whom, exactly?

> Second, the total possible footprint of entire classes of bugs is zero thanks to design features of Rust (the borrowck, sum types, data race prevention), except in a specifically delineated areas which often total zero in the vast majority of Rust programs.

And yet somehow the internet went down because of a program written in rust that didn’t validate input.

reply
mh2266
26 minutes ago
[-]
This just skips the:

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

from the original comment. Meanwhile all C code is implicitly “unsafe”. Rust at least makes it explicit!

But even if you ignore memory safety issues bypassed by unsafe, Rust forces you to handle errors, it doesn’t let you blow up on null pointers with no compiler protection, it allows you to represent your data exhaustively with sum types, etc etc etc

reply
irishcoffee
3 minutes ago
[-]
Isn’t rust proffered up as a systems language? One that begged to be accepted into the Linux kernel?

Don’t device drivers live in the Linux kernel tree?

So, unsafe code is generally approved in device driver code?

Why not just use C at that point?

reply
10000truths
58 minutes ago
[-]
The reason I really like Zig is because there's finally a language that makes it easy to gracefully handle memory exhaustion at the application level. No more praying that your program isn't unceremoniously killed just for asking for more memory - all allocations are assumed fallible and failures must be handled explicitly. Stack space is not treated like magic - the compiler can reason about its maximum size by examining the call graph, so you can pre-allocate stack space to ensure that stack overflows are guaranteed never to 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.

reply
incompatible
1 minute ago
[-]
I don't know Zig. The article says "Many people seem confused about why Zig should exist if Rust does already." But I'd ask instead why does Zig exist when C does already? It's just a "better" C? But has the drawback that makes C problematic for development, manual memory management? I think you are better off using a language with a garbage collector, unless your usage really needs manual management, and then you can pick between C, Rust, and Zig (and C++ and a few hundred others, probably.)
reply
kibwen
53 minutes ago
[-]
> No more praying that your program isn't unceremoniously killed just for asking for more memory - all allocations are assumed fallible and failures must be handled explicitly.

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.

reply
10000truths
25 minutes ago
[-]
Sure, but you can do the next best thing, which is to control precisely when and where those allocations occur. Even if the possibility of crashing is unavoidable, there is still huge operational benefit in making it predictable.

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

reply
wavemode
38 minutes ago
[-]
I imagine people who care about this sort of thing are happy to disable overcommit, and/or run Zig on embedded or specialized systems where it doesn't exist.
reply
dlisboa
32 minutes ago
[-]
There are far more people running/writing Zig on/for systems with overcommit than not. Most of the hype around Zig come from people not in the embedded world.
reply
wavemode
10 minutes ago
[-]
I never said that all Zig users care about recovering from allocation failure.
reply
Guvante
51 minutes ago
[-]
If you are pre-allocating Rust would handle that decently as well right?

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.

reply
vlovich123
1 hour ago
[-]
Re UB:

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

reply
oncallthrow
1 hour ago
[-]
> Other features common in modern languages, like tagged unions or syntactic sugar for error-handling, have not been added to Go.

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

reply
frizlab
2 minutes ago
[-]
Swift is great for that:

    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.
reply
edflsafoiewq
1 hour ago
[-]
Python's

    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.
reply
9rx
1 minute ago
[-]
But no context, so in the real world you need to write:

    try:
        f = open('foo.txt', 'w')
    except Exception as e:
        raise NecessaryContext(e)
Else your callers are in for a nightmare of a time.

(Perhaps there is a better way to express that, but this is Python: There is only supposed to be one way to do it. Then we've got even bigger problems.)

reply
verdverm
39 minutes ago
[-]
We were taught not to use exceptions for control flow, and reading a file which does not exist is a pretty normal thing to handle in code flow, rather than exceptions.

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

reply
pansa2
32 minutes ago
[-]
In Python, it’s common to use exceptions for control flow. Even exiting a loop is done via an exception: `StopIteration`.
reply
oncallthrow
52 minutes ago
[-]
> 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.

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

reply
reissbaker
34 minutes ago
[-]
Your Go example included zero information that Python wouldn't give you out-of-the-box. And FWIW, since this is "Go vs Rust vs Zig," both Rust and Zig allow for much more elegant handling than Go, while similarly forcing you to make sure your call succeeded before continuing.
reply
howenterprisey
1 hour ago
[-]
You can just as easily add context to the first example or skip the wrapping in the second.
reply
snuxoll
1 hour ago
[-]
Especially since the second example only gives you a stringly-typed error.

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

reply
oncallthrow
1 hour ago
[-]
I don't agree. There isn't a standard convention for wrapping errors in Rust, like there is in Go with fmt.Errorf -- largely because ? is so widely-used (precisely because it is so easy to reach for).

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.

reply
kstrauser
14 minutes ago
[-]
> There isn't a standard convention for wrapping errors in Rust

I have to say that's the first time I've heard someone say Rust doesn't have enough return types. Idiomatically, possible error conditions would be wrapped in a Result. `foo()?` is fantastic for the cases where you can't do anything about it, like you're trying to deserialize the user's passed-in config file and it's not valid JSON. What are you going to do there that's better than panicking? Or if you're starting up and can't connect to the configured database URL, there's probably not anything you can do beyond bombing out with a traceback... like `?` or `.unwrap()` does.

For everything else, there're the standard `if foo.is_ok()` or matching on `Ok(value)` idioms, when you want to catch the error and retry, or alert the user, or whatever.

But ? and .unwrap() are wonderful when you know that the thing could possibly fail, and it's out of your hands, so why wrap it in a bunch of boilerplate error handling code that doesn't tell the user much more than a traceback would?

reply
bargainbin
56 minutes ago
[-]
My experience aligns with this, although I often find the error being used for non-errors which is somewhat of an overcorrection, i.e. db drivers returning “NoRows” errors when no rows is a perfectly acceptable result of a query.

It’s odd that the .unwrap() hack caused a huge outage at Cloudflare, and my first reaction was “that couldn’t happen in Go haha” but… it definitely could, because you can just ignore returned values.

But for some reason most people don’t. It’s like the syntax conveys its intent clearly: Handle your damn errors.

reply
mh2266
50 minutes ago
[-]
I think the standard convention if you just want a stringly-typed error like Go is anyhow?

And maybe not quite as standard, but thiserror if you don’t want a stringly-typed error?

reply
fragmede
1 hour ago
[-]
yeah but which is faster and easier for a person to look at and understand. Go's intentionally verbose so that more complicated things are easier to understand.
reply
loeg
1 hour ago
[-]

  let mut file = File::create("foo.txt").context("failed to create file")?;
Of all the things I find hard to understand in Rust, this isn't one of them.
reply
snuxoll
52 minutes ago
[-]
Important to note that .context() is something from `anyhow`, not part of the stdlib.
reply
snuxoll
57 minutes ago
[-]
I don't really see it as any more or less verbose.

If I return Result<T, E> from a function in Rust I have to provide an exhaustive match of all the cases, unless I use `.unwrap()` to get the success value (or panic), or use the `?` operator to return the error value (possibly converting it with an implementation of `std::From`).

No more verbose than Go, from the consumer side. Though, a big difference is that match/if/etc are expressions and I can assign results from them, so it would look more like

    let a = match do_thing(&foo) {
      Ok(res) => res,
      Err(e) => return e
    }
instead of:

     a, err := do_thing(foo)
     if err != nil {
       return err // (or wrap it with fmt.Errorf and continue the madness
                  // of stringly-typed errors, unless you want to write custom
                  // Error types which now is more verbose and less safe than Rust).
    }
I use Go on a regular basis, error handling works, but quite frankly it's one of the weakest parts of the language. Would I say I appreciate the more explicit handling from both it and Rust? Sure, unchecked exceptions and constant stack unwinding to report recoverable errors wasn't a good idea. But you're not going to have me singing Go's praise when others have done it better.

Do not get me started on actually handling errors in Go, either. errors.As() is a terrible API to work around the lack of pattern matching in Go, and the extra local variables you need to declare to use it just add line noise.

reply
phibz
1 hour ago
[-]
I feel like this misses the biggest advantage of Result in rust. You must do something with it. Even if you want to ignore the error with unwrap() what you're really saying is "panic on errors".

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.

reply
oncallthrow
1 hour ago
[-]
Any sane Go team will be running errcheck, so I think this is a moot point.
reply
afavour
52 minutes ago
[-]
I think it’s still worth pointing out that one language includes it as a feature and the other requires additional tooling.
reply
dlisboa
46 minutes ago
[-]
Which can also be said about Rust and anyhow/thiserror. You won't see any decent project that don't use them, the language requires additional tooling for errors as well.
reply
tptacek
1 hour ago
[-]
I also prefer Rust's enums and match statements for error handling, but think that their general-case "ergonomic" error handling patterns --- the "?" thing in particular --- actually make things worse. I was glad when Go killed the trial balloon for a similar error handling shorthand. The good Rust error handling is actually wordier than Go's.
reply
sethops1
1 hour ago
[-]
I also like about Go that you can immediately see where the potential problem areas are in a page of code. Sure it's more verbose but I prefer the language that makes things obvious.
reply
NooneAtAll3
56 minutes ago
[-]
it's the other way around

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

reply
oncallthrow
49 minutes ago
[-]
> "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.

reply
YmiYugy
46 minutes ago
[-]
What is the context that the Go code adds here? When File::create or os.Create fails the errors they return already contain the information what and why something failed. So what information does "failed to create file: " add?
reply
edflsafoiewq
42 minutes ago
[-]
The error from Rust's File::create basically only contains the errno result. So it's eg. "permission denied" vs "failed to create file: permission denied".
reply
the_gipsy
1 hour ago
[-]
"Context" here is just a string. Debugging means grepping that string in the codebase, and praying that it's unique. You can only come up with so many unique messages along a stack.

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.

reply
oncallthrow
54 minutes ago
[-]
> Debugging means grepping that string in the codebase, and praying that it's unique.

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.

reply
the_gipsy
44 minutes ago
[-]
The go stdlib notoriously returns errors without wrapping. I think it has been shifting towards more wrapping more often, but still.

    err1 := foo()
    err2 := bar()
    if err1 != nil || err2 != nil {
        return err1  // if only err2 failed, returns nil!
    }
``` func process() error { err := foo() if err != nil { return err }

    if something {
        result, err := bar()  // new err shadows outer err
        if err != nil {
            return err
        }
        use(result)
    }
    
    if somethingElse {
        err := baz()  // another shadow
        log.Println(err)
    }
    
    return err  // returns foo's err (nil), baz's error lost
} ```
reply
librasteve
1 hour ago
[-]
I love this take - partly because I agree with it - but mostly because I think that this is the right way to compare PLs (and to present the results). It is honest in the way it ascribes strengths and weaknesses, helping to guide, refine, justify the choice of language outside of job pressures.

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?

reply
NooneAtAll3
53 minutes ago
[-]
what's WL?
reply
librasteve
1 hour ago
[-]
I tried to get an LLM to write a Raku chapter in the same vein - naah. Had to write it myself:

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.

reply
Rikudou
58 minutes ago
[-]
I think the Go part is missing a pretty important thing: the easiest concurrency model there is. Goroutines are one of the biggest reasons I even started with Go.
reply
auxiliarymoose
29 minutes ago
[-]
Agreed. Rob Pike presented a good talk "Concurrency is not Parallelism" which explains the motivations behind Go's concurrency model: https://youtu.be/oV9rvDllKEg

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.

reply
dmoy
2 hours ago
[-]
For a lot of stuff what I really want is golang but with better generics and result/error/enum handling like rust.
reply
vips7L
36 minutes ago
[-]
Me too. There’s a huge market for a natively compiled language with GC that has a better type system than Go.

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.

reply
Yoric
1 hour ago
[-]
Have you tried OCaml? With the latest versions, it also has an insanely powerful concurrency model. As far as I understand (I haven't looked at the benchmarks myself), it's also performance-competitive with Go.
reply
zozbot234
1 hour ago
[-]
There's also ReasonML if you want an OCaml with curly braces like C. But both are notably missing the high-performance concurrent GC that ships with Golang out of the box.
reply
throwaway894345
1 hour ago
[-]
How's the build tooling these days? Last I tried, it used some jbuild/dune + makefiles thing that was really painful to get up and running. Also there were multiple standard libraries and (IIRC) async runtimes that wouldn't play nicely together. The syntax and custom operators was also a thing that I could not stop stubbing my toes on--while I previously thought syntax was a relatively unimportant concern, my experience with OCaml changed my mind. :)

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?

reply
Yoric
1 hour ago
[-]
I'm re-discovering OCaml these days after an OCaml burnout quite a few years ago, courtesy of my then employer, so I'm afraid I can't answer these questions reliably :/

So far, I like what I've seen.

reply
myaccountonhn
1 hour ago
[-]
Ocaml community is chill and helpful, and dune works great with really good compilation speeds.

Its a really nice language

reply
evanmoran
1 hour ago
[-]
I thought the recent error proposal was quite interesting even if it didn't go through: https://github.com/golang/go/issues/71528

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)

reply
Rikudou
54 minutes ago
[-]
Didn't they say they're not accepting any new proposals for error handling?

I kinda got used to it eventually, but I'll never ever consider not having enums a good thing.

reply
Philpax
1 hour ago
[-]
You want https://github.com/borgo-lang/borgo, but that project is dead. You might be interested in Gleam?
reply
scythmic_waves
1 hour ago
[-]
Borgo [1] is basically that.

Though I think it's more of a hobby language. The last commit was > 1 year ago.

[1] https://news.ycombinator.com/item?id=40211891

reply
mixedCase
1 hour ago
[-]
OCaml is the closest match I'm aware of.
reply
throwaway894345
1 hour ago
[-]
I cautiously agree, with the caveat that while I thought I would really like Rust's error handling, it has been painful in practice. I'm sure I'm holding it wrong, but so far I have tried:

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

reply
iterance
1 hour ago
[-]
I will at least remark that adding a new error to an enum is not a breaking change if they are marked #[non_exhaustive]. The compiler then guarantees that all match statements on the enum contain a generic case.

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.

reply
grufkork
1 hour ago
[-]
Indeed, there's almost always a solution to "inergonomics" in Rust, but most are there to provide a guarantee or express an assumption to increase the chance that your code will do what's intended. While that safety can feel a bit exaggerated even for some large systems projects, for a lot of things Rust is just not the right tool if you don't need the guarantees.

On that topic, I've looked some at building games in Rust but I'm thinking it mostly looks like you're creating problems for yourself? Using it for implementing performant backend algorithms and containerised logic could be nice though.

reply
snuxoll
1 hour ago
[-]
> 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?

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.

reply
Yoric
1 hour ago
[-]
FWIW `fmt.Errorf("opening file %s: %w", filePath, err)` is pretty much equivalent to calling `err.with_context(|| format!("opening file {}", path))?` with anyhow.

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

reply
thijsr
1 hour ago
[-]
> I also don't love enums for errors because it means adding any new error type will be a breaking change

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.

reply
simgt
51 minutes ago
[-]
I personally like the flexibility it provides. You can go from very granular with an error type per function and an enum variant per error case, or very coarse with an error type for a whole module that holds a string. Use thiserror to make error types in libraries, and anyhow in programs to handle them.
reply
written-beyond
1 hour ago
[-]
You have to chill with rust. Just anyhow macro wrap your errors and just log them out. If you have a specific use case that relies on using that specific error just use that at the parent stack.
reply
oncallthrow
1 hour ago
[-]
vlang
reply
vibe_assassin
31 minutes ago
[-]
I'd rather read 3 lines of clear code than one line of esoteric syntactic sugar. I think regardless of what blogs say, Go's adoption compared to that of Rust or Zig speaks for itself
reply
publicdebates
1 hour ago
[-]
Good write up, I like where you're going with this. Your article reads like a recent graduate who's full of excitement and passion for the wonderful world of programming, and just coming into the real world for the first time.

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.

[1] https://www.youtube.com/watch?v=IroPQ150F6c

reply
gwd
1 hour ago
[-]
> 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.

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.

[1] https://research.swtch.com/generic

reply
mirashii
57 minutes ago
[-]
I’m not sure there’s anything clever that resolved the issues, they just settled on slow execution times by accepting a dynamic dispatch on generics.
reply
zozbot234
56 minutes ago
[-]
Ironically, the latest research by Google has now conclusively shown that Rust programmers aren't really any "slower" or less productive than Go programmers. That's especially true once you account for the entire software lifecycle, including production support and maintenance.
reply
rishabhaiover
21 minutes ago
[-]
> it is like C in that you can fit the whole language in your head.

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.

reply
vegabook
57 minutes ago
[-]
The last paragraph captures the essence that all the PL theory arguments do not. "Zig has a fun, subversive feel to it". It gives you a better tool than C to apply your amazing human skills, freely, whereas both Rust and Go are fundamentally sceptical about you.
reply
pm90
14 minutes ago
[-]
I still don’t get the point of zig, at least not from this post? I really don’t want to do memory management manually. I actually think rust is pretty well designed, but allows you to write very complex code. go tries really hard to keep it simple but at the cost of resisting modern features.
reply
int_19h
4 minutes ago
[-]
If you don't want to do memory management manually, then you're not the intended target audience for Zig. It's a language where any piece of code that needs to do heap allocation has to receive an allocator as an explicit argument in order to be able to allocate anything at all.
reply
gaanbal
2 hours ago
[-]
OP tried zig last and is currently most fascinated by it
reply
kachapopopow
1 hour ago
[-]
I could never get into zig purely because of the syntax and I know I am not alone, can someone explain the odd choices that were taken when creating zig?

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.

reply
usrnm
1 hour ago
[-]
I will never understand people bashing other languages for their syntax and readability and then saying that they prefer Rust. Async Rust is the ugliest and least readable language I've ever seen and I've done a lot of heavily templated C++
reply
kibwen
47 minutes ago
[-]
> Async Rust is the ugliest and least readable language I've ever seen and I've done a lot of heavily templated C++

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.

reply
von_lohengramm
1 hour ago
[-]
> and the 2nd most being the word 'try' instead of just ?

All control flow in Zig is done via keyword

reply
throwawaymaths
1 hour ago
[-]
'const expected = [_]u32{ 123, 67, 89, 99 };'

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)

reply
dwb
1 hour ago
[-]
These are extremely trivial, to the point that I don’t really know what you’re complaining about. What would expect or prefer?
reply
wrs
1 hour ago
[-]
> In Go, a slice is a fat pointer to a contiguous sequence in memory, but a slice can also grow, meaning that it subsumes the functionality of Rust’s Vec<T> type and Zig’s ArrayList.

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.

reply
auxiliarymoose
42 minutes ago
[-]
Writing "append(s, ...)" instead of "s = append(s, ...)" results in a compiler error because it is an unused expression. I'm not sure how a newbie could make this mistake since that code doesn't compile.
reply
dlisboa
33 minutes ago
[-]
> Go programmer attitude is "do what I said, and trust that I read the library docs before I said it".

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.

reply
ojosilva
1 hour ago
[-]
Fine, but there's a noticeable asymmetry in how the three languages get treated. Go gets dinged for hiding memory details from you. Rust gets dinged for making mutable globals hard and for conceptual density (with a maximally intimidating Pin quote to drive it home). But when Zig has the equivalent warts they're reframed as virtues or glossed over.

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

reply
ekropotin
1 hour ago
[-]
I pretty new to Rust and I’m wondering why global mutables are hard?

At first glance you can just use static variable of a type supporting interior mutability - RefCell, Mutex, etc…

reply
written-beyond
1 hour ago
[-]
I don't think it's specifically hard, it's more related to how it probably needed more plumbing in the language that authors thought would add to much baggage and let the community solve it. Like the whole async runtime debates
reply
umanwizard
56 minutes ago
[-]
> I pretty new to Rust and I’m wondering why global mutables are hard?

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.
reply
kennykartman
1 hour ago
[-]
I find this a nice read, but I don't think it captures the essence of these PL. To me it seems mostly a well crafted post to reach a point that basically says what people think of these languages: "go is minimal, rust is complex, zig is a cool, hot compromise". The usual.

It was fun to read, but I don't see anything new here, and I don't agree too much.

reply
shevy-java
41 minutes ago
[-]
Until someone creates a new language that is better than these ...
reply
dismalaf
16 minutes ago
[-]
One of these is not like the others...

Odin vs Rust vs Zig would be more apt, or Go vs Java vs OCaml or something...

reply
drnick1
34 minutes ago
[-]
Modern C++ is probably better than all of those if you need to interface with existing code and libraries, or need classic OOP.
reply
throwaway894345
1 hour ago
[-]
> [Go] is like C in that you can fit the whole language in your head.

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++?

reply
kanbankaren
1 hour ago
[-]
30 years in C/C++ here.

Give an example of UB code that you have committed in real life, not from blogs. I am genuinely curious.

reply
skywhopper
2 hours ago
[-]
Wow, this is a really good writeup without all the usual hangups that folks have about these languages. Well done!
reply
reeeli
1 hour ago
[-]
if the languages were creations of LLMs, what would be your (relatively refined) chain(s) of (indulgently) critical thought?
reply
raggi
1 hour ago
[-]
I really hate the anti-RAII sentiments and arguments. I remember the Zig community lead going off about RAII before and making claims like "linux would never do this" (https://github.com/torvalds/linux/blob/master/include/linux/...).

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.

reply
sestep
7 minutes ago
[-]
Heh, sounds like you'd love the work-in-progress I'm about to present at MWPLS 2025 :)
reply
iainmerrick
43 minutes ago
[-]
Have you tried Swift? It has the sort of pragmatic-but-safe-by-default approach you’re talking about.
reply
frizlab
14 seconds ago
[-]
And it is a joy to use, truly.
reply
bsder
32 minutes ago
[-]
> So RAII isn't the big evil monster, and we need to stop talking about RAII, globals, etc, in these ways.

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.

reply
zozbot234
14 minutes ago
[-]
> 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.

reply
echelon
2 hours ago
[-]
> Many people seem confused about why Zig should exist if Rust does already. It’s not just that Zig is trying to be simpler. I think this difference is the more important one. Zig wants you to excise even more object-oriented thinking from your code.

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 !

reply
Quothling
1 hour ago
[-]
> Rust is not hard. Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.

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.

reply
unshavedyak
2 hours ago
[-]
Rust is hard in that it gives you a ton of rope to hang yourself with, and some people are just hell bent on hanging themselves.

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.

reply
nicoburns
1 hour ago
[-]
That seems like an odd characterization of Rust. The borrow checker and all the other type safety features, as well as features like send/sync are all about not giving you rope to hang yourself with.
reply
unshavedyak
1 hour ago
[-]
The rope in my example is complexity. Ie choosing to use "all teh features" when you don't need or perhaps even want to. Eg sometimes a simple clone is fine. Sometimes you don't need to opt for every generic and performance minded feature Rust offers - which are numerous.

Though, i think my statement is missing something. I moved from Go to Rust because i found that Rust gave me better tooling to encapsulate and reuse logic. Eg Iterators are more complex under the hood, but my observed complexity was lower in Rust compared to Go by way of better, more generalized code reuse. So in this example i actually found Go to be more complex.

So maybe a more elaborated phrase would be something like Rust gives you more visible rope to hang yourself with.. but that doesn't sound as nice. I still like my original phrase heh.

reply
1313ed01
56 minutes ago
[-]
I would love to see a language that is to C what Rust is to C++. Something a more average human brain like mine can understand. Keep the no-gc memory safety things, but simplify everything else a thousand times.

Not saying that should replace Rust. Both could exist side by side like C and C++.

reply
mh2266
38 minutes ago
[-]
I feel like it is the opposite, Go gives you a ton of rope to hang yourself with and hopefully you will notice that you did: error handing is essentially optional, there are no sum types and no exhaustiveness checks, the stdlib does things like assume filepaths are valid strings, if you forget to assign something it just becomes zero regardless of whether it’s semantically reasonable for your program to do that, no nullability checking enforcement for pointers, etc.

Rust OTOH is obsessively precise about enforcing these sort of things.

Of course Rust has a lot of features and compiles slower.

reply
kace91
1 hour ago
[-]
One question about your functional point: where can I learn functional programming in terms of organization of large codebases?

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.

reply
myaccountonhn
1 hour ago
[-]
There are a lot of lectures/speeches by the creator of elm and Richard Feldman that talk about how to think "functionally"

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.

reply
the__alchemist
1 hour ago
[-]
Same. Zig's niche is in the vein of languages that encourages using pointers for business logic. If you like this style, Rust and most other new languages aren't an option.
reply
Yoric
1 hour ago
[-]
> I feel like Zig is for the C / C++ developers that really dislike Rust.

Also my feeling. Writing this as a former C++ developer who really likes Rust :)

reply
999900000999
1 hour ago
[-]
Rust is hard because it's just difficult to read.

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

reply
tiltowait
1 hour ago
[-]
> Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.

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.

reply
awesome_dude
2 hours ago
[-]
> 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.

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)

reply
0x457
2 hours ago
[-]
Reads like a very surface level take with a minor crush on Rob Pike.
reply
Aperocky
1 hour ago
[-]
Anecdotally, as a result of the traits that made it hard to learn for humans, Rust is actually a great language for LLM.

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.

reply
damslunk
48 minutes ago
[-]
Yeah that's an interesting point, it feels like it should be even better than it is now (I might be ignorant of the quality of the best coding agents atm).

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

reply
ux266478
1 hour ago
[-]
Generally a good writeup, but the article seems a bit confused about undefined behavior.

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

reply
iainmerrick
54 minutes ago
[-]
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. [...] That's it. That's all there is to UB.

You don’t think that’s pretty bad?

reply
publicdebates
1 hour ago
[-]
I think it's common to be taught that UB is very bad when you're new, partly to simplify your debugging experience, partly to help you understand and mentally demarcate the boundaries of what the language allows and doesn't allow, and partly because there are many Standards-Purists who genuinely avoid UB. But from my own experience, UB just means "consult your compiler to see what it does here because this question is beyond our pay grade."

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.

reply
mirashii
53 minutes ago
[-]
> But from my own experience, UB just means "consult your compiler to see what it does here because this question is beyond our pay grade."

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.

reply
publicdebates
43 minutes ago
[-]
I understand, but you have to see how you would be considered one of the Standards-Purists that I was talking about, right? If Microsoft makes a guarantee in their documentation about some behavior of UB C code, and this guarantee is dated to about 14 years ago, and I see many credible people on the internet confirming that this behavior does happen and still happens, and these comments are scattered throughout those past 14 years, I think it's safe to say I can rely on that behavior, as long as I'm okay with a little vendor lock-in.
reply
mirashii
3 minutes ago
[-]
> If Microsoft makes a guarantee in their documentation about some behavior of UB C code

But do they? Where?

More likely, you mean that a particular compiler may say "while the standard says this is UB, it is not UB in this compiler". That's something wholly different, because you're no longer invoking UB.

reply
kibwen
43 minutes ago
[-]
> But from my own experience, UB just means "consult your compiler to see what it does here because this question is beyond our pay grade."

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.

reply