logoalt Hacker News

Thoughts on Go vs. Rust vs. Zig

256 pointsby yurivishyesterday at 9:40 PM286 commentsview on HN

Comments

kibwenyesterday at 10:43 PM

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

show 5 replies
deathanatostoday at 12:24 AM

> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust:

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.

show 3 replies
10000truthsyesterday at 11:18 PM

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.

show 5 replies
vlovich123yesterday at 10:41 PM

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

wrsyesterday at 11:08 PM

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

show 4 replies
librasteveyesterday at 10:32 PM

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?

show 2 replies
Rikudouyesterday at 11:17 PM

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.

show 3 replies
dmoyyesterday at 10:09 PM

For a lot of stuff what I really want is golang but with better generics and result/error/enum handling like rust.

show 10 replies
kachapopopowyesterday at 10:41 PM

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.

show 4 replies
publicdebatesyesterday at 10:27 PM

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

show 1 reply
ojosilvayesterday at 10:27 PM

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

show 1 reply
gaanbalyesterday at 10:13 PM

OP tried zig last and is currently most fascinated by it

rishabhaioveryesterday at 11:54 PM

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

show 1 reply
vibe_assassinyesterday at 11:45 PM

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

pm90today at 12:01 AM

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.

show 1 reply
oncallthrowyesterday at 10:58 PM

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

show 14 replies
vegabookyesterday at 11:19 PM

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.

show 4 replies
tialaramextoday at 12:46 AM

> In both Go and Rust, allocating an object on the heap is as easy as returning a pointer to a struct from a function.

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 ?

show 1 reply
dmixtoday at 2:19 AM

> If the number of questions online about “Go vs. Rust” or “Rust vs. Zig” is a reliable metric

The human brain demands "vs" articles

teleforcetoday at 12:37 AM

>I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust:

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

https://news.ycombinator.com/item?id=23494490 [2]

kennykartmanyesterday at 11:10 PM

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.

shevy-javayesterday at 11:35 PM

Until someone creates a new language that is better than these ...

sheepscreektoday at 3:14 AM

> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust

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.

dismalafyesterday at 11:59 PM

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

throwaway894345yesterday at 10:35 PM

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

show 1 reply
throwaway2037today at 12:47 AM

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

qaqtoday at 1:19 AM

with LLMs started defaulting to go for most projects

Rust for WASM

Zig is what I'd use if I started a greenfield DBMS project

skywhopperyesterday at 10:12 PM

Wow, this is a really good writeup without all the usual hangups that folks have about these languages. Well done!

reeeliyesterday at 10:29 PM

if the languages were creations of LLMs, what would be your (relatively refined) chain(s) of (indulgently) critical thought?

drnick1yesterday at 11:41 PM

Modern C++ is probably better than all of those if you need to interface with existing code and libraries, or need classic OOP.

raggiyesterday at 10:19 PM

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.

show 3 replies
echelonyesterday at 10:06 PM

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

show 8 replies
badmonstertoday at 2:02 AM

[dead]

neonsunsettoday at 12:53 AM

[dead]

black_13today at 12:06 AM

[dead]

0x457yesterday at 10:06 PM

Reads like a very surface level take with a minor crush on Rob Pike.

Aperockyyesterday at 10:50 PM

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.

show 1 reply
ux266478yesterday at 11:04 PM

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.

show 2 replies