Hacker News new | ask | show | jobs
by loup-vaillant 12 days ago
> Go is a better C already

It’s not. Garbage collection made sure of it. I believe everyone agrees that a "better C" has to have manual memory management. Most even rule out Go as a systems language because of GC.

Of course, I’m pretty sure Go is much better than C at some problems. But there’s no way in hell it supersedes it.

3 comments

Nope, only the anti-GC religion would agree to that.

Most of those folks would never manage to replicate something like Xerox Cedar on their own, from 1980 in their beloved 2026 computers.

Thankfully we have the likes of Apple and Google, that have a my way or the highway education system for such developers, that want to go through their magical gardens.

There are roughly two orders of magnitude more microcontrollers in the world running code than application processors. GC is not acceptable on a microcontroller due to the extreme resource constraints. The GP post is correct, Go can replace some use cases for C, but it does not replace all use cases and GC is part of the reason.
> GC is not acceptable on a microcontroller due to the extreme resource constraints.

Dynamic memory allocation on a microcontroller can be treacherous full stop, but if you have accepted the tradeoffs of using dynamic memory allocation then why not GC? You wouldn't want gc[1]-style GC, but there are other GC algorithms that can work well enough on micros. The Go spec calls for garbage collection, but it doesn't say how you have to collect garbage.

> but it does not replace all use cases

It could replace all use cases. There are arguably much better tools for many jobs[2], but if you had to the language isn't going to stop you.

[1] gc the compiler, not GC garbage collection. Naming is the hardest problem in CS.

[2] Some would say all jobs, but for the sake of discussion let's agree that Go can be a reasonable choice at least sometimes.

> * but if you have accepted the tradeoffs of using dynamic memory allocation then why not GC?*

That’s the thing: many do not use dynamic allocation at all. Did you know for instance that you could write an entire modern cipher suite using only a small (<300 bytes), strictly bounded stack allocation?

As for those who do use the "heap", many can do so with a strict stack discipline, so it’s not really a heap. Reason being, the environment has an extremely low call stack budget, so all bigger allocations happens in another stack, located in the heap.

Then there are arenas, which are much simpler to implement than a general allocator, tend to have less overhead (both space and runtime) than malloc() or a GC, though I reckon they can still run out of space. Still, less treacherous than uncontrolled use of malloc().

Most of all though, even if the compiler can do mighty static region analysis to minimise heap usage and GC overhead, heck, even perhaps reduce it to absolutely zero in some cases, there’s still the problem of the programming interface: how does the programmer know about stuff like max heap usage? I guess the compiler could tell them, but then how do they fix it if usage exceeds the limit, or is unbounded? The wouldn’t be any clear link from heap usage to the source code, or it would be so diffuse it would be hard to determine where to begin.

And of course, a GC’ed program would likely rely on many more pointers under the hood than one that manages its memory manually, which means significant space overhead even if the GC implementation itself is perfect.

You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.

> many do not use dynamic allocation at all.

Exactly. You don’t need GC if you don’t produce garbage. And since Go uses stack-based memory like C you can avoid producing garbage just the same.

And if you do need some kind of special memory allocation then you can do the same tricks you do with C. You still don’t have to use the built-in allocator.

All the bellyaching about GC is silly because you don’t need it in the first place. The cries are like saying C isn’t suitable for microcontrollers because its standard library includes malloc… Just don’t use it.

> You want me to write something for an ESP-32, it’s gonna be in something like C, Rust, or Zig.

Sure. The Go creators were explicit that Go is designed for building network systems. That doesn’t mean it is impossible to use for anything else, but it was optimized for a particular niche. You will feel more comfortable using languages designed for microcontrollers when programming for microcontrollers.

But it’s not GC you aren’t using that gets in the way. You don’t have to use every language feature just because it is there. Rust doesn’t become unusable for programming because it has a string type and you only need integers. Just don’t use it.

"Just don’t use it" is a valid argument, if it’s clear when you’re using it or not. It’s easy not to use `malloc()`: just don’t call it, and don’t call dependencies that call it. Same for Rust’s string: don’t construct strings, and don’t depend on stuff that does.

In some cases, avoiding heap allocation is just impossible. Take OCaml: last time I checked (over 10 years ago) there are two kinds of values: integers, and pointers to heap allocated objects. That’s basically it. Even floats are allocated on the heap (except when inside an array that’s the only optimisation). So unless you only do integer operations and avoid constructing any kind of data structure, you can’t avoid heap allocation.

Other languages are much more conservative than OCaml in this regard. That with value types and all. If they’re clear enough which constructs don’t trigger heap allocation, and the set of thereof is rich enough to allow significant work, then yes "just don’t use the GC" is a valid option. Not all languages (I mean implementations, but we can conflate the two in most cases) fulfil those two conditions. Maybe Go does.

Go already runs on microcontrollers, but _just like C_, care must be taken to avoid using features that are not compatible with microcontrollers (such as dynamic memory).
Most of those folks would never manage to replicate something like Xerox Cedar on their own, from 1980 in their beloved 2026 computers.

What are you talking about

> I believe everyone agrees that a "better C" has to have manual memory management.

This seems completely wrong. Manual memory management is perhaps C's most famous issue. I'm sure _someone_ thinks manual memory management is a feature rather than a bug, but I don't think there's any broad consensus about this at all.

Moreover, Go gives you a lot of levers to control your allocations, and with some care (probably less care than writing correct C code) you can avoid allocating at all (this is how Go's own runtime works).

> Manual memory management is perhaps C's most famous issue

C's most famous issue is that it doesn't have a dynamic memory management concept at all. You can implement manual memory management on top of C, like you can in any language, but it is not a core feature. If C had manual memory management then it could be reasoned about, which would avoid many of the pitfalls associated with memory management being bolted on top.

Whether that is a feature or a bug probably depends on what kind of software you are building. If you are targeting a PC, a dynamic memory management concept in the language can be useful. If you are targeting a small microcontroller where you don't want dynamic allocation then things can get a bit weird having language features that need to be disabled or having to rely on outside processes (code review, linters, etc.) to control use.

But the general consensus these days does seem to be that a language should include a dynamic memory management concept, even if it is sometimes disabled. So a "better C" likely would gain memory management, but you make a fair point that it doesn't have to be manual in nature.

> C's most famous issue is that it doesn't have a dynamic memory management concept at all. You can implement manual memory management on top of C, like you can in any language, but it is not a core feature. If C had manual memory management then it could be reasoned about, which would avoid many of the pitfalls associated with memory management being bolted on top

It seems like you’re conflating dynamic and manual memory management? Or maybe you are using these terms in a way I’m unfamiliar with. C definitely has manual memory management via malloc and free. It can be reasoned about, but doing so correctly is very difficult (this is what Rust’s ownership system formalizes, after all). I don’t think this is controversial?

> If you are targeting a small microcontroller where you don't want dynamic allocation then things can get a bit weird having language features that need to be disabled or having to rely on outside processes (code review, linters, etc.) to control use.

C has dynamic memory (again, malloc and free) and thus it is in the same boat as other languages that have to take care to avoid using dynamic memory when writing embedded code (a decade and a half ago I was an embedded engineer using C and C++).

> It seems like you’re conflating dynamic

Perhaps I should have said heap memory to be more clear, but the intent was to recognize that C does have memory management (the stack), but it manages it automatically. However, I don't think the intent was actually lost as you specifically called out malloc, which was directionally correct. So I guess there was no need to be more clear as the message was delivered just fine.

> C definitely has manual memory management via malloc

No. malloc isn't part of the C language, it is a library function and one that isn't guaranteed to be available at that.

> It can be reasoned about, but doing so correctly is very difficult

To the best of our knowledge it is impossible. That is why Rust's claim to fame is adding a model necessary to make reasoning about memory possible.

> C has dynamic memory (again, malloc and free)

Again, malloc and free are not language features. They are bolted on top. You can bolt malloc and free onto every language under the sun. You can even bolt malloc and free onto Rust, but you will then break the ability to reason about its memory if you use it.

malloc and free are part of the C standard library. I don’t see the point in picking nits between language features you don’t have to use and standard library functions that you don’t have to use.
When you called attention to Rust you clearly understood the point of picking nits. Why pretend you have no idea what we are talking about now?

malloc/free, not being a language feature, cannot be reasoned about. Since the larger discussion is about Go, consider using malloc/free in Go. Of course you can do it. There is nothing stopping you. Since it is not part of a language it can be bolted on to any language, not just C. But if you do, it should be obvious you can no longer reason about the program in terms of how memory is used. If there was a way to reason about it, C would already have all the same memory guarantees that Rust does.

> It can be reasoned about, but doing so correctly is very difficult […]. I don’t think this is controversial?

It’s actually much easier than 95% of programmers think. The trick is to stop using fine grained allocations all the time, which are very difficult without tricks like RAII or a borrow checker (not to mention the high runtime overhead), and start using arenas instead. https://www.rfleury.com/p/untangling-lifetimes-the-arena-all...

> I'm sure _someone_ thinks manual memory management is a feature rather than a bug,

I reckon mandatory manual memory management is not ideal. But the ability to manage some memory manually, including in cases that require something more flexible than a stack, is definitely a feature.

I believe every willing user of Zig, Odin, and Rust would agree with me on this one.

> […] with some care […] you can avoid allocating at all […]

As long as it’s crystal clear from the source code, or I have a tool that tells me which line of mine is responsible for the allocation.

> As long as it’s crystal clear from the source code, or I have a tool that tells me which line of mine is responsible for the allocation.

The tool is the escape analyzer, but I don’t think anyone has made it ergonomic yet.

Then it needs to be made standard. Without that, or an equivalent solution, garbage collected languages will remain unusable in constrained environments.
I'm not sure I agree (or maybe I misunderstand--I assume you're saying "the tool needs to be standard"?), but people use non-standard tools in C all the time. Every build tool or package management solution in C is non-standard. Every memory analyzer or static analysis solution is non-standard.

I think the real impediment isn't related to garbage collection, but it's just ecosystem inertia and industry knowledge inertia--the same forces that inhibit Rust adoption or C++ adoption or Ada adoption. Does that seem reasonable, or am I misunderstanding? What do you think?

I mean it needs to be made standard because of how bloody useful that is. More people need to be made aware (industry knowledge inertia), and it needs to be usable pretty much everywhere (ecosystem inertia).

C and C++ start out so unsafe, I can hardly use them on substantial projects without sanitisers now. Sanitisers need to be made standard so I can use them everywhere.

Garbage collected languages start out with unpredictable memory usage, making them unusable on constrained environments without the relevant memory analysers (static or dynamic). Such tools need to be made standard so garbage collection may be an option on more platforms.

> Of course, I’m pretty sure Go is much better than C at some problems. But there’s no way in hell it supersedes it.

C is great when you need to do manual memory management. Most software written today doesn't need to do manual memory management. (And frankly, for the typical software project, manual memory management is a liability.)

> Most software written today doesn't need to do manual memory management.

Need? Not really, not on the desktop (embedded is a different story entirely). But there are still benefits, as well as costs.

Thing is, most programmers have no idea what the actual costs an benefits are, because they have a hopelessly incomplete view of what manual memory management actually is. To them, and this includes long time C++ programmers, manual memory management means calling the general purpose allocator for every little object. At best they’d batch allocation in arrays or vectors, but otherwise it’s RAII for most stuff, and new/malloc() for anything more complicated. This kind of memory management have high costs and low benefits. In some cases the runtime overhead is even greater than using a GC.

And then there are arenas. Much easier to deal with, much lower overhead, much safer, more predictable… They make the cost/benefit analysis completely different, to the point you could ask yourself why you would even bother depending on a GC and the heavy runtime that goes with it.

https://www.dgtlgrove.com/p/untangling-lifetimes-the-arena-a...

> C is great when you need to do manual memory management.

Is it still, today? There are other manual memory languages today with better safety records and ergonomics. Plus a host of new and better features.

I think C is really great if you need C. For libraries, for knowledge, for size, for compatibility, for "simplicity", for fun, for whatever.

But other than having a strong reason to want C, on a purely language feature comparison, it's not great.

Compatibility is a big one. Probably the biggest reason to chose C over Zig or Rust today. But that can be remedied if the new language has a C compilation target.
Rust is memory safe, but not "manual memory management." IE, the vast, vast majority of Rust doesn't use pointers.