Go is still not good
blog.habets.se
blog.habets.se
But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. That said, it's a _feeling_ I have, and maybe Go would be much worse if it had solved all these quirks. To be fair, I see more leniency in fixing quirks in the last few years, like at some point I didn't think we'd ever see generics, or custom iterators, etc.
The points about RAM and portability seem mostly like personal grievances though. If it was better, that would be nice, of course. But the GC in Go is very unlikely to cause issues in most programs even at very large scale, and it's not that hard to debug. And Go runs on most platforms anyone could ever wish to ship their software on.
But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often.
I'd say that it's entirely the other way around: they stuck to the practical convenience of solving the problem that they had in front of them, quickly, instead of analyzing the problem from the first principles, and solving the problem correctly (or using a solution that was Not Invented Here).
Go's filesystem API is the perfect example. You need to open files? Great, we'll create
func Open(name string) (*File, error)
function, you can open files now, done. What if the file name is not valid UTF-8, though? Who cares, hasn't happen to me in the first 5 years I used Go.Nothing? Neither Go nor the OS require file names to be UTF-8, I believe
They could support passing filename as `string | []byte`. But wait, go does not even have union types.
This is one of the minor errors in the post.
I've said this before, but much of Go's design looks like it's imitating the C++ style at Google. The comments where I see people saying they like something about Go it's often an idiom that showed up first in the C++ macros or tooling.
I used to check this before I left Google, and I'm sure it's becoming less true over time. But to me it looks like the idea of Go was basically "what if we created a Python-like compiled language that was easier to onboard than C++ but which still had our C++ ergonomics?"
Then make it valid UTF-8. If you try to solve the long tail of issues in a commonly used function of the library its going to cause a lot of pain. This approach is better. If someone has a weird problem like file names with invalid characters, they can solve it themselves, even publish a package. Why complicate 100% of uses for solving 0.01% of issues?
I know it's mostly a matter of tastes, but darn, it feels horrible. And there are no default parameter values, and the error hanling smells bad, and no real stack trace in production. And the "object orientation" syntax, adding some ugly reference to each function. And the pointers...
It took me back to my C/C++ days. Like programming with 25 year old technology from back when I was in university in 1999.
The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way. People expect a map/filter method rather than a loop with off by one risks, a type system with the smartness of typescript (if less featured and more heavily enforced), error handling is annoying, and so on.
I get that it’s tough to implement some of those features without opening the way to a lot of “creativity” in the bad sense. But I feel like go is sometimes a hard sell for this reason, for young devs whose mother language is JavaScript and not C.
I agree with this. I feel like Go was a very smart choice to create a new language to be easy and practical and have great tooling, and not to be experimental or super ambitious in any particular direction, only trusting established programming patterns. It's just weird that they missed some things that had been pretty well hashed out by 2009.
Map/filter/etc. are a perfect example. I remember around 2000 the average programmer thought map and filter were pointlessly weird and exotic. Why not use a for loop like a normal human? Ten years later the average programmer was like, for loops are hard to read and are perfect hiding places for bugs, I can't believe we used to use them even for simple things like map, filter, and foreach.
By 2010, even Java had decided that it needed to add its "stream API" and lambda functions, because no matter how awful they looked when bolted onto Java, it was still an improvement in clarity and simplicity.
Somehow Go missed this step forward the industry had taken and decided to double down on "for." Go's different flavors of for are a significant improvement over the C/C++/Java for loop, but I think it would have been more in line with the conservative, pragmatic philosophy of Go to adopt the proven solution that the industry was converging on.
Do they? After too many functional battles I started practicing what I'm jokingly calling "Debugging-Driven Development" and just like TDD keeps the design decisions in mind to allow for testability from the get-go, this makes me write code that will be trivially easy to debug (specially printf-guided debugging and step-by-step execution debugging)
Like, adding a printf in the middle of a for loop, without even needing to understand the logic of the loop. Just make a new line and write a printf. I grew tired of all those tight chains of code that iterate beautifully but later when in a hurry at 3am on a Sunday are hell to decompose and debug.
It feels often like the two principles they stuck/stick to are "what makes writing the compiler easier" and "what makes compilation fast". And those are good goals, but they're only barely developer-oriented.
Like, yes, those ideas have frequently been driven too far and have led to their own pain points. But people also seem to frequently rediscover that removing them entirety will lead to pain, too.
The go language and its runtime is the only system I know that is able to handle concurrency with multicore cpus seamlessly within the language, using the CSP-like (goroutine/channel) formalism which is easy to reason with.
Python is a mess with the gil and async libraries that are hard to reason with. C,C++,Java etc need external libraries to implement threading which cant be reasoned with in the context of the language itself.
So, go is a perfect fit for the http server (or service) usecase and in my experience there is no parallel.
Elixir handling 2 million websocket connections on a single machine back in 2015 would like to have a word.[1] This is largely thanks to the Erlang runtime it sits atop.
Having written some tricky Go (I implemented Raft for a class) and a lot of Elixir (professional development), it is my experience that Go's concurrency model works for a few cases but largely sucks in others and is way easier to write footguns in Go than it ought to be.
[1]: https://phoenixframework.org/blog/the-road-to-2-million-webs...
Java does not need external libraries to implement threading, it's baked into the language and its standard libraries.
What do you mean by this for Java? The library is the runtime that ships with Java, and while they're OS threads under the hood, the abstraction isn't all that leaky, and it doesn't feel like they're actually outside the JVM.
Working with them can be a bit clunky, though.
I believe it’s the only system you know. But it’s far from the only one.
I thought it was a seldom mentioned fact in Go that CSP systems are impossible to reason about outside of toy projects so everyone uses mutexes and such for systemic coordination.
I'm not sure I've even seen channels in a production application used for anything more than stopping a goroutine, collecting workgroup results, or something equally localized.
Ah, what????
I agree.
The Go std-lib is fantastic.
Also no dependency-hell with Go, unlike with Python. Just ship an oven-ready binary.
And what's the alternative ?
Java ? Licensing sagas requiring the use of divergent forks. Plus Go is easier to work with, perhaps especially for server-side deployments.
Zig ? Rust ? Complex learning curve. And having to choose e.g. Rust crates re-introduces dependency hell and the potential for supply-chain attacks.
Yeah, these are sagas only, because there is basically one, single, completely free implementation anyone uses on the server-side and it's OpenJDK, which was made 100% open-source and the reference implementation by Oracle. Basically all of Corretto, AdoptOpenJDK, etc are just builds of the exact same repository.
People bringing this whole license topic up can't be taken seriously, it's like saying that Linux is proprietary because you can pay for support at Red Hat..
I’m only a casual user of both but how are rust crates meaningfully different from go’s dependency management?
you can go `uv run script.py` and it'll automatically fetch the libraries and run the script in a virtual environment.
Still no match for Go though, shipping a single cross-compiled binary is a joy. And with a bit of trickery you can even bundle in your whole static website in it :) Works great when you're building business logic with a simple UI on top.
I have absolutely no idea how go would solve this problem, and in fact I don't think it does at all.
> The Go std-lib is fantastic.
I have seen worse, but I would still not call it decent considering this is a fairly new language that could have done a lot more.
I am going to ignore the incredible amount of asinine and downright wrong stuff in many of the most popular libraries (even the basic ones maintained by google) since you are talking only about the stdlib.
On the top of my head I found inconsistent tagging management for structs (json defaults, omitzero vs omitempty), not even errors on tag typos, the reader/writer pattern that forces you to to write custom connectors between the two, bzip2 has a reader and no writer, the context linked list for K/V. Just look at the consistency of the interfaces in the "encoding" pkg and cry, the package `hash` should actually be `checksum`. Why does `strconv.Atoi`/ItoA still exist? Time.Add() vs Time.Sub()...
It chock full of inconsistencies. It forces me to look at the documentation every single time I don't use something for more than a couple of days. No, the autocomplete with the 2-line documentation does not include the potential pitfalls that are explained at the top of the package only.
And please don't get me started on the wrappers I had to write around stuff in the net library to make it a bit more consistent or just less plain wrong. net/url.Parse!!! I said don't make my start on this package! nil vs NoBody! ARGH!
None of this is stuff at the language level (of which there is plenty to say).
None of it is a dealbreaker per se, but it adds attrition and becomes death by a billion cuts.
I don't even trust any parser written in go anymore, I always try to come up with corner cases to check how it reacts, and I am often surprised by most of them.
Sure, there are worse languages and libraries. Still not something I would pick up in 2025 for a new project.
Yes, My favourite is the `time` package. It's just so elegant how it's just a number under there, the nominal type system truly shines. And using it is a treat. What do you mean I can do `+= 8*time.Hour` :D
The code was on the hot path of their central routing server handling Billions (with a B) messages in a second or something crazy like that.
You're not building Discord, the GC will most likely never be even a blip in your metrics. The GC is just fine.
I don't have a lot of experience with the malloc languages at scale, but I do know that heat fragmentation and GC fragmentation are very similar problems.
There are techniques in GC languages to avoid GC like arena allocation and stuff like that, generally considered non-idiomatic.
I got insta rejected in interview when i said this in response to interview panels question about 'thoughts about golang' .
Like they said, 'interview is over' and showed me the (virtual) door. I was stunned lol. This was during peak golang mania . Not sure what happened to rancherlabs .
It’s part trying to keep a common direction and part fear that dislike of their tech risks the hire not staying for long.
I don’t agree with this approach, don’t get me wrong, but I’ve seen it done and it might explain your experience.
This tends to be true for most languages, even the ones with easier concurrency support. Using it correctly is the tricky part.
I have no real problem with the portability. The area I see Go shining in is stuff like AWS Lambda where you want fast execution and aren't distributing the code to user systems.
In what universe?
Is it the best or most robust or can you do fancy shit with it? No
But it works well enough to release reliable software along with the massive linter framework that's built on top of Go.
I quite often see devs introducing them in other languages like TypeScript, but it just doesn't work as well when it's introduced in userland (usually you just end up with a small island of the codebase following this standard).
Go has chosen explicit over implicit everywhere except initialization—the one place where I really needed "explicit."
I shouldn't fault the creators. They did what they did, and that is all and good. I am more shocked by the way it has exploded in adoption.
Would love to see a coffeescript for golang.
It's not viable to use, but: https://github.com/borgo-lang/borgo
I think this is a fine "fail-closed" way of language design. For example, Python has gone the other way and language complexity has gotten pretty bad since the small-language days. Trust what you are, don't try to please everyone, lest you become something like C++.
Clojure is good in this respect.
[1] ZGC has basically decoupled the heap size from the pause time, at that point you get longer pauses from the OS scheduler than from GC.
Well, so long as you don't care about compatibility with the broad ecosystem, you can write a perfectly fine Optional yourself:
type Optional[Value any] struct {
value Value
exists bool
}
// New empty.
func New[Value any]() Optional[Value] {}
// New of value.
func Of[Value any](value Value) Optional[Value] {}
// New of pointer.
func OfPointer[Value any](value *Value) Optional[Value] {}
// Only general way to get the value.
func (o Optional[Value]) Get() (Value, bool) {}
// Get value or panic.
func (o Optional[Value]) MustGet() Value {}
// Get value or default.
func (o Optional[Value]) GetOrElse(defaultValue Value) Value {}
// JSON support.
func (o Optional[Value]) MarshalJSON() ([]byte, error) {}
func (o *Optional[Value]) UnmarshalJSON(data []byte) error {}
// DB support.
func (o *Optional[Value]) Scan(value any) error {}
func (o Optional[Value]) Value() (driver.Value, error) {}
But you probably do care about compatibility with everyone else, so... yeah it really sucks that the Go way of dealing with optionality is slinging pointers around.For JSON, you can't encode Optional[T] as nothing at all. It has to encode to something, which usually means null. But when you decode, the absence of the field means UnmarshalJSON doesn't get called at all. This typically results in the default value, which of course you would then re-encode as null. So if you round-trip your JSON, you get a materially different output than input (this matters for some other languages/libraries). Maybe the new encoding/json/v2 library fixes this, I haven't looked yet.
Also, I would usually want Optional[T]{value:nil,exists:true} to be impossible regardless of T. But Go's type system is too limited to express this restriction, or even to express a way for a function to enforce this restriction, without resorting to reflection, and reflection has a type erasure problem making it hard to get right even then! So you'd have to write a bunch of different constructors: one for all primitive types and strings; one each for pointers, maps, and slices; three for channels (chan T, <-chan T, chan<- T); and finally one for interfaces, which has to use reflection.
You hear that Rob Pike? LOL. All those years he shat on Java, it was so irritating. (Yes schadenfreude /g)
In fact this was so surprising to me is that I only found out about it when I wrote code that processed files in a loop, and it started crashing once the list of files got too big, because defer didnt close the handles until the function returned.
When I asked some other Go programmers, they told me to wrap the loop body in an anonymus func and invoke that.
Other than that (and some other niggles), I find Go a pleasant, compact language, with an efficient syntax, that kind of doesn't really encourage people trying to be cute. I started my Go journey rewriting a fairly substantial C# project, and was surprised to learn that despite it having like 10% of the features of C#, the code ended up being smaller. It also encourages performant defaults, like not forcing GC allocation at every turn, very good and built-in support for codegen for stuff like serialization, and no insistence to 'eat the world' like C# does with stuff like ORMs that showcase you can write C# instead of SQL for RDBMS and doing GRPC by annotating C# objects. In Go, you do SQL by writing SQL, and you od GRPC by writing protobuf specs.
Right now it's function scope; if you need it lexical scope, you can wrap it in a function.
Suppose it were lexical scope and you needed it function scope. Then what do you do?
You can just introduce a new scope wherever you want with {} in sane languages, to control the required behavior as you wish.
Defer a bulk thing at the function scope level, and append files to an array after opening them.
?
2. mechanic is tied to call stack / stack unwinding
3. it feels natural when you're coming from C with `goto fail`
(yes it annoys me when I want to defer in a loop & now that loop body needs to be a function)
Since they didn't want to have a 'proper' RAII unwinding mechanism, this is the crappy compromise they came up with.
It can be also a source of bugs where you hang onto something for longer than intended - considering there's no indication of something that might block in Go, you can acquire a mutex, defer the release, and be surprised when some function call ends up blocking, and your whole program hangs for a second.
We pre-allocate a bunch of static buffers and re-use them. But that leads to a ton of ownership issues, like the append footgun mentioned in the article. We've even had to re-implement portions of the standard library because they allocate. And I get that we have a non-standard use case, and most programmers don't need to be this anal about memory usage. But we do, and it would be really nice to not feel like we're fighting the language.
You made a poor choice of language for the problem. It'd be a good fit for C/C++/Rust/Zig.
I've heard the term "beaten path" used for these languages, or languages that an organization chooses to use and forbids the use of others.
What I would absolutely love is a compacting garbage collector, but my understanding is Go can’t add that without breaking backwards compatibility, and so likely will never do that.
It’s faster than Node or Python, with a better type system than either. It’s got a much easier learning curve than Rust. It has a good stdlib and tooling. Simple syntax with usually only one way to do things. Error handling has its problems but I still prefer it over Node, where a catch clause might receive just about anything as an “error”.
Am I missing a language that does this too or more? I’m not a Go fanatic at all, mostly written Node for backends in my career, but I’ve been exploring Go lately.
I feel like I could write this same paragraph about Java or C#.
Anyway, assuming you're talking about TypeScript, I'm surprised to hear that you prefer Go's type system to TypeScript's. There are definitely cases where you can get carried away with TypeScript types, but due to that expressiveness I find it much more productive than Go's type system (and I'd make the same argument for Rust vs. Go).
That’s the selling point for me. If I’m coming to a legacy code as that no one working wrote, I pray it is go because then it just keeps working through upgrading the compiler and generally the libraries used.
There are languages with fewer warts, but they're usually more complicated (e.g. Rust), because most of Go's problems are caused by its creators' fixation with simplicity at all costs.
For NodeJS development, you would typically write it in Typescript - which has a very good type system.
Personally I have also written serverside C# code, which is a very nice experience these days. C# is a big language these days though.
Given Python's substantial improvements recently, I would put it far ahead of the structural typing done in Go, personally.
Agree on node/TS error handling. It’s super whack
Go is a case of the emperor having no clothes. Telling people that they just don’t get it or that it’s a different way of doing things just doesn’t convince me. The only thing it has going for it is a simple dev experience.
As someone who's been doing Go since 2015, working on dozens of large codebases counting probably a million lines total, across multiple teams, your criticisms do not ring true.
Go is no worse than C when it comes to extensibility, or C# or Java for that matter. Go programs are only extensible to the extent (ha) developers design their codebases right. Certainly, Go trades expressivity for explicitness more than some languages. You're encouraged to have fewer layers of abstraction and be more concrete and explicit. But in no way does that impede being able to extend code. The ability to write modular, extensible programs is a skill that must be learned, not something a programming language gives you for free.
It sounds like you worked on a poorly constructed codebase and assumed it was Go's fault.
Go is a reasonably performant language that makes it pretty straightforward to write reliable, highly concurrent services that don't rely on heavy multithreading - all thanks to the goroutine model.
There really was no other reasonably popular, static, compiled language around when Google came out.
And there still barely is - the only real competitor that sits in a similar space is Java with the new virtual threads.
Languages with async/await promise something similar, but in practice are burdened with a lot of complexity (avoiding blocking in async tasks, function colouring, ...)
I'm not counting Erlang here, because it is a very different type of language...
So I'd say Go is popular despite the myriad of shortcomings, thanks to goroutines and the Google project street cred.
The change from Java 8 to 25 is night and day. And the future looks bright. Java is slowly bringing in more language features that make it quite ergonomic to work with.
Every single piece of Go 1.x code scraped from the internet and baked in to the models is still perfectly valid and compiles with the latest version.
Which Google uses far more commonly than Go, still to this day.
My experience is mostly with C#, but async/await works very well there in my experience. You do need to know some basics there to avoid problem, but that's the case for essentially every kind of concurrency. They all have footguns.
Usually, as here, objections to go take the form a technically-correct-but-ultimately-pedantic arguments.
The positives of go are so overwhelmingly high magnitude that all those small things basically don’t matter enough to abandon the language.
Go is good enough to justify using it now while waiting for the slow-but-steady stream of improvements from version to version to make life better.
- Zero values, lack of support for constructors
- Poor handling of null
- Mutability by default
- A static type system not designed with generics in mind
- `int` is not arbitrary precision [1]
- The built-in array type (slices) has poorly considered ownership semantics [2]
Notable mentions:
- No sum types
- No string interpolation
Debugging is a nightmare because it refuses to even compile if you have unused X (which you always will have when you're debugging and testing "What happens if I comment out this bit?").
The bureaucracy is annoying. The magic filenames are annoying. The magic field names are annoying. The secret hidden panics in the standard library are annoying. The secret behind-your-back heap copies are annoying (and SLOW). All the magic in go eventually becomes annoying, because usually it's a naively repurposed thing (where they depend on something that was designed for a different purpose under different assumptions, but naively decided to depend on its side effects for their own ever-so-slightly-incompatible machinery - like special file names, and capitalization even though not all characters have such a thing .. was it REALLY such a chore to type "pub" for things you wanted exposed?).
Now that AI has gotten good, I'm rather enjoying Rust because I can just quickly ask the AI why my types don't match or a gnarly mutable borrow is happening - rather than spending hours poring over documentation and SO questions.
i made a cli tool[1] to mitigate this problem, it can be integrated into an ide quite easily, currently it has (neo)vim integration described in the readme and a vs code plugin-companion [2] which both can serve as an example and inspiration for creating an integration for your ide of choice
[1] https://github.com/vipkek/gouse
[2] https://marketplace.visualstudio.com/items?itemName=looshch....
edit: formatting
Btw, AI sucks on GO. One would have guessed that such a simple lang would suit ChatGPT. Turns out ChatGPT is much better at Java, C#, Pyhton and many other langs than GO.
Go people will yell at you because you aren't using the right tool.
Yeah, Go is too rigid with their principles.
There's no single 'best language', and it depends on what your use-cases are. But I'd say that for many typical backend tasks, Go is a choice you won't really regret, even if you have some gripes with the language.
* The Dremel is approachable: I don't have to worry about cutting off my hand with the jigsaw or set up a jig with the circular saw. I don't have to haul my workpiece out to the garage.
* The Dremel is simple: One slider for speed. Apply spinny bit to workpiece.
* The Dremel is fun: It fits comfortably in my hand. It's not super loud. I don't worry about hurting myself with it. It very satisfyingly shaves bits of stuff off things.
In so many respects, the Dremel is a great tool. But 90% of the time when I use it, it ends up taking my five times as long (but an enjoyable 5x!) and the end result is a wobbly scratchy mess. I curse myself for not spending the upfront willpower to use the right tool for the job.
I find myself doing this with all sorts of real and software tools: Over-optimizing for fun and ease-of-entry and forgetting the value of the end result and using the proper tool for the job.
I think of this as the "Dremel effect" and I try to be mindful of it when selecting tools.
I think there is little to no chance it can hold on to its central vision as the creators "age out" of the project, which will make the language worse (and render the tradeoffs pointless).
I think allowing it to become pigeon holed as "a language for writing servers" has cost and will continue to cost important mindshare that instead jumps to Rust or remains in Python or etc.
Maybe it's just fun, like harping on about how bad Visual Basic was, which was true but irrelevant, as the people who needed to do the things it did well got on with doing so.
Some critique is definitely valid, but some of it just sounds like they didn't take the time to grasp the language. It's trade offs all the way. For example there is a lot I like about Rust, but still no my favorite language.
That said I really wish there was a revamp where they did things right in terms of nil, scoping rules etc. However, they've commited to never breaking existing programs (honorable, understandable) so the design space is extremely limited. I prefer dealing with local awkwardness and even excessive verbosity over systemic issues any day.
I don't think the article sounds like someone didn't take the time to grasp the language. It sounds like it's talking about the kind of thing that really only grates on you after you've seriously used the language for a while.
I quite like Go and use it when I can. However, I wish there were something like Go, without these issues. It's worth talking about that. For instance, I think most of these critiques are fair but I would quibble with a few:
1. Error scope: yes, this causes code review to be more complex than it needs to be. It's a place for subtle, unnecessary bugs.
2. Two types of nil: yes, this is super confusing.
3. It's not portable: Go isn't as portable as C89, but it's pretty damn portable. It's plenty portable to write a general-purpose pre-built CLI tool in, for instance, which is about my bar for "pragmatic portability."
4. Append ownership & other slice weirdness: yes.
5. Unenforced `defer`: yes, similar to `err`, this introduces subtle bugs that can only be overcome via documentation, careful review, and boilerplate handling.
6. Exceptions on top of err returns: yes.
7. utf-8: Hasn't bitten me, but I don't know how valid this critique is or isn't.
8. Memory use: imo GC is a selling-point of the language, not a detriment.
I'm surprised people in these comments aren't focusing more on the append example.
(I realise this isn’t who is hiring, but email in bio)
myfunc(arg: string): Value | Err
I really try not to throw anymore with typescript, I do error checking like in Go. When used with a Go backend, it makes context switching really easy...
What has been an issue for me, though, is working with private repositories outside GitHub (and I have to clarify that, because working with private repositories on GitHub is different, because Go has hardcoded settings specifically to make GitHub work).
I had hopes for the GOAUTH environment variable, but either (1) I'm more dumb and blind than I thought I already was, or (2) there's still no way to force Go to fetch a module using SSH without trying an HTTPS request first. And no, `GOPRIVATE="mymodule"` and `GOPROXY="direct"` don't do the trick, not even combined with Git's `insteadOf`.
``` machine {server} # e.g. gitlab.com login {username} password {read_only_api_key} # Must be actual key and not an ENV var ```
Worked consistently, but not a solution we were thrilled with.
:^/
> I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Golang's biggest shortcoming is the fact that it touches bare metal isn't visible clearly enough. It provides many high level features which makes this ambience of "we got you" but fails on delivering proper education to its users that they are going to have a dirt on their hands.
Take a slice for example: even in naming it means "part of" but in reality it's closer to "box full of pointers" what happens when you modify pointer+1? Or "two types of nil"; there is a difference between having two bytes (simplification), one of struct type and the other of address to that struct and having just a NULL - same as knowing that house doesn't exist and being confident that house exists and saying it's in the middle of the volcano beneath the ocean.
The Foo99 critique is another example. If you'd want to have not 99 loop but 10 billion loops each with mere 10 bytes you'd need 100GiB of memory just to exit it. If you'd reuse the address block you'd only use... 10 bytes.
I also recommend trying to implement lexical scope defer in C and putting them in threads. That's a big bottle of fun.
I think that it ultimately boils down to what kind of engineer one wants to be. I don't like hand holding and rather be left on my own with a rain of unit tests following my code so Go, Zig, C (from low level Languages) just works for me. Some prefer Rust or high level abstractions. That's also fine.
But IMO poking at Go that it doesn't hide abstractions is like making fun of football of being child's play because not only it doesn't have horses but also has players using legs instead of mallets.
Author here.
No, this is not where it comes from. I've been coding C form more than 30 years, Go for maybe 12-15, and currently prefer Rust. I enjoy C++ (yes, really) and getting all those handle-less knives to fit together.
No, my critique of Go is that it did not take the lessons learned from decades of theory, what worked and didn't work.
I don't fault Go for its leaky abstractions in slices, for example. I do fault it for creating bad abstraction APIs in the first place, handing out footguns when they are avoidable. I know to avoid the footgun of appending to slices while other slices of the same array may still be accessible elsewhere. But I think it's indefensible to have created that footgun in the year Go was created.
Live long enough, and anybody will make a silly mistake. "Just don't make a mistake" is not an option. That's why programming language APIs and syntax matters.
As for bare metal; Go manages to neither get the benefits possible of being high level, and at the same time not being suitable for bare metal.
It's a missed opportunity. Because yes, in 2007 it's not like I could have pointed to something that was strictly better for some target use cases.
These days, it seems like languages keep chasing paradigms and over adapt to moving targets.
Look at what Rust and Swift have become. C# has stayed relatively sane somehow, but it's not what I'd call indepedent.
But everyone knows in their heart of hearts that a few small language warts definitely don't outweigh Go's simplicity and convenience. Do I wish it had algebraic data types, sure, sure. Is that a deal-breaker, nah. It's the perfect example of something that's popular for a reason.
It is easily one of the most productive languages. No fuss, no muss, just getting stuff done.
It just feels sloppy and I'm worried I'm going to make a mistake.
i think you can take these[1][2][3][4] official advices and extrapolate to other cases
[1] https://go.dev/wiki/CodeReviewComments#receiver-type
[2] https://google.github.io/styleguide/go/decisions#receiver-ty...
Now I always use pointers consistently for the readability.
And also when I want a value with stable identity I'd use a pointer.
type
StatusCodes = (Success, Ongoing, Done)
Go in 2025, type StatusCodes int
const (
Success StatusCodes = iota
Ongoing
Done
)So: The way Go presents it is confusing, but this behavior makes sense, is correct, will never be changed, and is undoubtedly depended on by correct programs.
The confusing thing for people use to C++ or C# or Java or Python or most other languages is that in Go nil is a perfectly valid pointer receiver for a method to have. The method resolution lookup happens statically at compile time, and as long as the method doesn't try to deref the pointer, all good.
It still works if you assign to an interface.
package main
import "fmt"
type Dog struct {}
type Cat struct {}
type Animal interface {
MakeNoise()
}
func (*Dog) MakeNoise() { fmt.Println("bark") }
func (*Cat) MakeNoise() { fmt.Println("meow") }
func main() {
var d *Dog = nil
var c *Cat = nil
var i Animal = d
var j Animal = c
d.MakeNoise()
c.MakeNoise()
i.MakeNoise()
j.MakeNoise()
}
This will print bark
meow
bark
meow
But the interface method lookup can't happen at compile time. So the interface value is actually a pair -- the pointer to the type, and the instance value. The type is not nil, hence the interface value is something like (&Cat,nil) and (&Dog,nil) in each case, which is not the interface zero value, which is (nil, nil).But it's super confusing because Go type cooerces a nil struct value to a non-nil (&type, nil) interface value. There's probably some naming or syntax way to make this clearer.
But the behavior is completely reasonable.
You were right, it's a niche and therefore pretty much irrelevant issue. They may as well have rejected Python due to its "significant whitespace".
- As someone who’s worked with C/C++ and Fortran, I think all these languages have their own challenges—Go’s simplicity trades off against Rust’s safety guarantees, for example.
- Could someone share a real-world example where Go’s design caused a production issue that Rust or another language would’ve avoided?
- I’m curious how these trade-offs play out in practice.
Sorry, I don't do Go/Rust coding, still on C/C++/Fotran.
A simple one, if you create two separate library in Go and try to link with an application, you will have a terrible time.
I've ran into this same issue: https://github.com/golang/go/issues/65050
I like Go and Rust, but sometimes I feel like they lack tools that other languages have just because they WANT to be different, without any real benefit.
Whenever I read Go code, I see a lot more error handling code than usual because the language doesn't have exceptions...
And sometimes Go/Rust code is more complex because it also lacks some OOP tools, and there are no tools to replace them.
So, Go/Rust has a lot more boilerplate code than I would expect from modern languages.
For example, in Delphi, an interface can be implemented by a property:
type
TMyClass = class(TInterfacedObject, IMyInterface)
private
FMyInterfaceImpl: TMyInterfaceImplementation; // A field containing the actual implementation
public
constructor Create;
destructor Destroy; override;
property MyInterface: IMyInterface read FMyInterfaceImpl implements IMyInterface;
end;
This isn't possible in Go/Rust. And the Go documentation I read strongly recommended using Composition, without good tools for that.This "new way is the best way, period ignore good things of the past" is common.
When MySQL didn't have transactions, the documentation said "perform operations atomically" without saying exactly how.
MongoDB didn't have transactions until version 4.0. They said it wasn't important.
When Go didn't have generics, there were a bunch of "patterns" to replace generics... which in practice did not replace.
The lack of inheritance in Go/Rust leaves me with the same impression. The new patterns do not replace the inheritance or other tools.
"We don't have this tool in the language because people used it wrong in the old languages." Don't worry, people will use the new tools wrong too!
Similarly, if a field implements a trait in Rust, you can expose it via `AsRef` and `AsMutRef`, just return a reference to it.
These are not ideal tools, and I find the Go solution rather unintuitive, but they solve the problems that I would've solved with inheritance in other languages. I rarely use them.
Some learnings. Don't pass sections of your slices to things that mutate them. Anonymous functions need recovers. Know how all goroutines return.
It's hard to find a language that will satisfy everyone's needs. Go I find better for smaller, focused applications/utilities... can definitely see how it would cause problems at an "enterprise" level codebase.
https://github.com/pannous/goo/
• errors handled by truthy if or try syntax • all 0s and nils are falsey • #if PORTABLE put(";}") #end • modifying! methods like "hi".reverse!() • GC can be paused/disabled • many more ease of use QoL enhancements
Go is a super productive powerhouse for me.
These sorts of articles have been commonplace even before Go released 1.0 in 2013. In fact, most (if not all) of these complaints could have been written identically back then. The only thing missing from this post that could make me believe it truly was written in 2013 would be a complaint about Go not having generics, which were added a few years ago.
People on HN have been complaining about Go since Go was a weird side-project tucked away at Google that even Google itself didn't care about and didn't bother to dedicate any resources to. Meanwhile, people still keep using it and finding it useful.
It is infuriating because it is close to being good, but it will never get there - now due to backwards compatibility.
Also Rob Pike quote about Go's origins is spot on.
AI solved my issues with carpal tunnel.
And when I'm feeling fancy, I don't even type, just command AI by voice. "handle error case".
The green threads are very interesting since you can create 1000s of them at a low cost and that makes different designs possible.
I think this complaining about defer is a bit trivial. The actual major problem for me is the way imports work. The fact that it knows about github and the way that it's difficult to replace a dependency there with some other one including a local one. The forced layout of files, cmd directories etc etc.
I can live with it all but modules are the things which I have wasted the most time and struggled the most.
Use `replace` in `go.mod`, or `go.work` if you're hacking on it locally?
You don't need to have a cmd directory. I see it a lot in Go projects but I'm not sure why.
But that’s all this is, is a list of annoyances the author experiences when using Go. Great, write an article about the problems with Go, but don’t say “therefore it’s a bad language”.
While I agree with many of the points brought up, none of them seems like such a huge issue that it’s even worth discussing, honestly. So you have different taste the the original designers. Who cares? What language do you say is better? I can find just as many problems with that language.
Also, defer is great.
Its main virtues are low cognitive load and encouraging simple straightforward ways of doing things, with the latter feeding into the former.
Languages with sophisticated powerful type systems and other features are superior in a lot of ways, but in the hands of most developers they are excuses to massively over-complicate everything. Sophomore developers (not junior but not yet senior) love complexity and will use any chance to add as much of it as they can, either to show off how smart they are, to explore, or to try to implement things they think they need but actually don't. Go somewhat discourages this, though devs will still find a way of course.
Experienced developers know that complexity is evil and simplicity is actually the sign of intelligence and skill. A language with advanced features is there to make it easier and simpler to express difficult concepts, not to make it more difficult and complex to express simple concepts. Every language feature should not always be used.
You end up creating these elegant abstractions that are very seductive from a programmer-as-artist perspective, but usually a distraction from just getting the work done in a good enough way.
You can tell that the creators of Go are very familiar with engineer psychology and what gets them off track. Go takes away all shiny toys.
bar, err := foo()
if err != nil {
return err
}
if err = foo2(); err != nil {
return err
}
Sounded more like a nitpicking.If you really care about scope while being able to use `bar` later down, the code should be written as:
bar, err := foo()
if err != nil {
return err
}
err = foo2() // Just reuse `err` plainly
if err != nil {
return err
}
which actually overwrites `err`, opposite to "shadowing" it.The confusing here, is that the difference between `if err != nil` and `if err = call(); err != nil` is not just style, the later one also introduces a scope that captures whatever variables got created before `;`.
If you really REALLY want to use the same `if` style, try:
if bar, err := foo(); err != nil {
return err
} else if bar2, err := foo2(); err != nil {
return err
} else {
[Use `bar` and `bar2` here]
return ...
}With all of that, Go becomes the perfect language for the age of LLMs writing all the code. Let the LLMs deal with all the boilerplate and misery of Go, while at the same time its total lack of elegance is also well suited to LLMs which similarly have the most dim notions of code elegance.
package main
import "fmt"
type I interface{}
type S struct{}
func main() {
var i I
var s *S
fmt.Println(s, i) // nil nil
fmt.Println(s is nil, i is nil, s == i) // t,t,f: Not confusing anymore?
i = s
fmt.Println(s, i) // nil nil
fmt.Println(s is nil, i is nil, s == i) // t,f,t: Still not confusing?
}
Of course, this means you have to precisely define the semantics of `is` and `==`:- `is` for interfaces checks both value and interface type.
- `==` for interfaces uses only the value and not the interface type.
- For structs/value, `is` and `==` are obvious since there's only a value to check.
I also review a lot of Go code. I see these problems all the time from other people.
Here's the accompanying playground: https://go.dev/play/p/Kt93xQGAiHK
If you run the code, you will see that calling read() on ControlMessage causes a panic even though there is a nil check. However, it doesn't happen for Message. See the read() implementation for Message: we need to have a nil check inside the pointer-receiver struct methods. This is the simplest solution. We have a linter for this. The ecosystem also helps, e.g protobuf generated code also has nil checks inside pointer receivers.
First one - you have an address to a struct, you pass it, all good.
Second case: you set address of struct to "nil". What is nil? It's an address like anything else. Maybe it's 0x000000 or something else. At this point from memory perspective it exists, but OS will prevent you from touching anything that NULL pointer allows you to touch.
Because you don't touch ANYTHING nothing fails. It's like a deadly poison in a box you don't open.
Third example id the same as second one. You have a IMessage but it points to NULL (instead NULL pointing to deadly poison).
And in fourth, you finally open the box.
Is it magic knowledge? I don't think so, but I'm also not surprised about how you can modify data through slice passing.
IMO the biggest Go shortcoming is selling itself as a high level language, while it touches more bare metal that people are used to touch.
Use the language where it makes sense. You do know what if you have an issue that the language fails at, you can solve that particular problem in another language and.. call that code?
We used to have a node ts service. We had some computationally heavy stuff, we moved that one part to Go because it was good for that ONE thing. I think later someone ported that one thing to Rust and it became a standalone project.
Idk. It’s just code. Nobody really cares, we use these tools to solve problems.
Talk about hyperbole.
(something that's not a minor error, that someone else pointed out, is that Python isn't strictly refcounted. Yeah, that's why emphasized "almost" and "pretty much". I can't do anything about that kind of critique)
I can see why people pick it but its major step up in convenience rather than major step up in evolution programming language itself
The distinction you're making here does not exist IMO. Convenience is the entire point of design.
It's a good language for teams, for sure, though.
In the meantime he also wrote about Rust: "I’ve not found anything frustrating or stupid in Rust yet."
So he can just use Rust and stop using Go completely. Problem solved. No need to write those hates. Go lang is cleary not good for him and it's ok.
> The standard library does that. fmt.Print when calling .String(), and the standard library HTTP server does that, for exceptions in the HTTP handlers.
Apart from this most doesn't seem that big of a deal, except for `append` which is truly a bad syntax. If you doing it inplace append don't return the value.
This means that if you do:
func (f Foo) String() string {
some.Lock()
t := get_something()
some.Unlock()
t = transform_it(t)
return t
}
And `get_something()` panics, then the program continues with a locked mutex. There are more dangerous things than a deadlocked program, of course.It's non-optional to use defer, and thus write exception safe code. Even if you never use exceptions.
Don't use it I guess and ignore all the X is not good posts for language X you do decide to use?
I don't get how you can assign an interface to be a pointer to a structure. How does that work? That seems like a compile error. I don't know much about Go interfaces.
I really like Go. It scratches every itch that I have. Is it the language for your problems? I don't know, but very possibly that answer is "no".
Go is easy to learn, very simple (this is a strong feature, for me) and if you want something more, you can code that up pretty quickly.
The blog article author lost me completely when they said this:
> Why do I care about memory use? RAM is cheap.
That is something that only the inexperienced say. At scale, nothing is cheap; there is no cheap resource if you are writing software for scale or for customers. Often, single bytes count. RAM usage counts. CPU cycles count. Allocations count. People want to pretend that they don't matter because it makes their job easier, but if you want to write performant software, you better have that those cpu cache lines in mind, and if you have those in mind, you have memory usage of your types in mind.
Well if maximalist performance tuning is your stated goal, to the point that single bytes count, I would imagine Go is a pretty terrible choice? There are definitely languages with a more tunable GC and more cache line friendly tools than Go.
But honestly, your comment reads more like gatekeeping, saying someone is inexperienced because they aren't working with software at the same scale as you. You sound equally inexperienced (and uninterested) with their problem domain.
the nil issue. An interface, when assigned a struct, is no longer nil even if that struct is nil - probably a mistake. Valid point.
append in a func. Definitely one of the biggest issues is that slices are by ref. They did this to save memory and speed but the append issue becomes a monster unless abstracted. Valid point.
err in scope for the whole func. You defined it, of course it is. Better to reuse a generic var than constantly instantiate another. The lack of try catch forces you to think. Not a valid point.
defer. What is the difference between a scope block and a function block? I'll wait.
Go has a good-enough standard library, and Go can support a "pile-of-if-statements" architecture. This is all you need.
Most enterprise environments are not handled with enough care to move beyond "pile-of-if-statements". Sure, maybe when the code was new it had a decent architecture, but soon the original developers left and then the next wave came in and they had different ideas and dreamed of a "rewrite", which they sneakily started but never finished, then they left, and the 3rd wave of developers came in and by that point the code was a mess and so now they just throw if-statements onto the pile until the Jira tickets are closed, and the company chugs along with its shitty software, and if the company ever leaks the personal data of 100 million people, they aren't financially liable.
> Over the decades I have lost data to tools skipping non-UTF-8 filenames. I should not be blamed for having files that were named before UTF-8 existed.
Umm.. why blame Go for that?
What I intended to say with this is that ignoring the problem if invalid UTF-8 (could be valid iso8859-1) with no error handling, or other way around, has lost me data in the past.
Compare this to Rust, where a path name is of a different type than a mere string. And if you need to treat it like a string and you don't care if it's "a bit wrong" (because it's for being shown to the user), then you can call `.to_string_lossy()`. But it's be more hard to accidentally not handle that case when exact name match does matter.
When exactness matters, `.to_str()` returns `Option<&str>`, so the caller is forced to deal with the situation that the file name may not be UTF-8.
Being sloppy with file name encodings is how data is lost. Go is sloppy with strings of all kinds, file names included.
bar, err := foo()
if err != nil {
return err
}
if err := foo2(); err != nil {
return err
}
The above (which declares a new value of err scoped to the second if statement) should compile right? What is it that he's complaining about?EDIT: OK, I think I understand; there's no easy way to have `bar` be function-scoped and `err` be if-scoped.
I mean, I'm with him on the interfaces. But the "append" thing just seems like ranting to me. In his example, `a` is a local variable; why would assigning a local variable be expected to change the value in the caller? Would you expect the following to work?
int func(a *MyStruct) {
a = &MyStruct{...}
}
If not why would you expect `a = apppend(a, ...)` to work?I think you may need to re-read. My point is that it DOES change the value in the caller. (well, sometimes) That's the problem.
If someone really doesn't like the reuse of err, there's no reason why they couldn't create separate variable, e.g. err_foo and err_foo2. There's not no reason to not reuse err.
What in the javascript is this.
package main
import "fmt"
type Foo struct{
Name string
}
func (f *Foo) Kaboom() {
fmt.Printf("hello from Kaboom, f=%s\n", f.Name)
}
func NewKaboom() interface{ Kaboom() } {
var p *Foo = nil
return p
}
func main() {
obj := NewKaboom()
fmt.Printf("obj == nil? %v\n", obj == nil)
// The next line will panic (because method receives nil *Foo)
obj.Kaboom()
}
go run fred.go
obj == nil? false
panic: runtime error: invalid memory address or nil pointer dereferenceIt turns out to be nothing but a misunderstanding of what the fmt.Println() statement is actually doing. If we use a more advanced print statement then everything becomes extremely clear:
package main
import (
"fmt"
"github.com/k0kubun/pp/v3"
)
type I interface{}
type S struct{}
func main() {
var i I
var s *S
pp.Println(s, i) // (*main.S)(nil) nil
fmt.Println(s == nil, i == nil, s == i) // true true false
i = s
pp.Println(s, i) // (*main.S)(nil) (*main.S)(nil)
fmt.Println(s == nil, i == nil, s == i) // true false true
}
The author of this post has noted a convenience feature, namely that fmt.Println() tells you the state of the thing in the interface and not the state of the interface, mistaken it as a fundamental design issue and written a screed about a language issue that literally doesn't exist.Being charitable, I guess the author could actually be complaining that putting a nil pointer inside a nil interface is confusing. It is indeed confusing, but it doesn't mean there are "two types" of nil. Nil just means empty.
It's not about Printf. It's about how these two different kind of nil values sometimes compare equal to nil, sometimes compare equal to each other, and sometimes not
Yes there is a real internal difference between the two that you can print. But that is the point the author is making.
What are you trying to clarify by printing the types? I know what the types are, and that's why I could provide the succinct weird example. I know what the result of the comparisons are, and why.
And the "why" is "because there are two types of nil, because it's a bad language choice".
I've seen this in real code. Someone compares a variable to nil, it's not, and then they call a method (receiver), and it crashes with nil dereference.
Edit, according to this comment this two-types-of-null bites other people in production: https://news.ycombinator.com/item?id=44983576
I chuckled
Never had any problems with Go as it makes me millions each year.
First time its assigned nil, second time its overwritten in case there's an error in the 2nd function. I dont see the authors issue? Its very explicit.
After checking for nil, there's no reason `err` should still be in scope. That's why it's recommended to write `if err := foo(); err != nil`, because after that, one cannot even accidentally refer to `err`.
I'm giving examples where Go syntactically does not allow you to limit the lifetime of the variable. The variable, not its value.
You are describing what happens. I have no problem with what happens, but with the language.
yeah no. you need an acyclic structure to maybe guarantee this, in CPython. other Python implementations are more normal in that you shouldn't rely on finalizers at all.
> It is possible (though not recommended!) for the __del__() method to postpone destruction of the instance by creating a new reference to it. This is called object resurrection.
[0]: https://docs.python.org/3/reference/datamodel.html#object.__...
Yes, yes. Hence the words "almost" and "pretty much". For exactly this reason.
Python sucks balls but it has more fanboys than a K-pop idol. Enforced whitespace? No strong typing? A global interpreter lock? Garbage error messages? A package repository you can't search (only partially because it's full of trash packages and bad forks) with random names and no naming convention? A complete lack of standardization in installing or setting up applications/environments? 100 lines to run a command and read the stout and stderr in real time?
The real reason everyone uses Python and Go is because Google used them. Otherwise Python looks like BASIC with objects and Go is an overhyped niche language.
:Error variable Scope -> Yes can be confusing at the beginning, but if you have some experience it doesnt really matter. Would it be cool to scope it down?`Sure, but it feels like here is something blown up to an "issue" where i would see other things to alot more important for the go team to revisit. Regarding the error handling in go, some hate it , some love it : i personally like it (yes i really do) so i think its more a preference than a "bad" thing.
:Two types of nil -> Funny, i never encountered this in > 10 years of go with ALOT of work in pointer juggling, so i wonder in which reality this hits your where it cant be avoided. Tho confusing i admit
:It’s not portable -> I have no opinion here since i work on unix systems only and i have my compiled binaries specific shrug dont see any issue here either.
:append with no defined ownership -> I mean... seriously? Your test case, while the results may be unexpected, is a super wierd one. Why you you append a mid field, if you think about what these functions do under the hood your attemp actualyl feels like you WANT to procude strange behaviour and things like that can be done in any language.
:defer is dumb -> Here i 100% agree - from my pov it leads to massive resource wasting and in certain situations it can also create strange errors, but im not motivated to explain this - ill just say defer, while it seems usefull, from my pov is a bad thing and should not be used.
:The standard library swallows exceptions, so all hope is lost -> "So all hope is lost" i mean you already left the realm of objectiveness long before tbut this really tops it. I wrote some quite big go applications and i never had a situation where i could not handle an exception simply by adjusting my code in a way that i prevent it from even happening. Again - i feel like someone is just in search of things to complain that could simply be avoided. (also in case someone comes up with a super specific probably once in a million case, well alrways keep in mind that language design doesnt orient on the least occuring thing).
:Sometimes things aren’t UTF-8 -> I wont bother to read another whole article, if its important include an example. I have dealth with different encodings (web crawler) and i could handle all of them.
:Memory use -> What you describe is one of the design decisions im not absolutly happy with, the memory handling. But than, one of my golang projects is an in memory graph storage/database - which in one of my cases run for ~2years without restart and had about 18GB of dataset stored in it. It has a lot of mutex handling (regarding your earlier complain with exxceptions, never had one) and it btw run as backend of a internet facing service so it wasnt just fed internal data.
--------------------
Finally i wanne say : often things come down to personal preference. I could spend days raging about javascript, java, c++ or some other languages, but whatfor? Pick the language that fits your use case and your liking, dont pick one that doesnt and complain about it.
Also , just to show im not just a big "golang is the best" fanboy, because it isnt - there are things to critizize like the previously mentioned memory handling.
While i still think you just created memory leaks in your app, golang had this idea of "arenas" which would enable the code to manage memory partly himself and therefor developt much more memory efficient applications. This has stalled lately and i REALLY hope the go team will pick it up again and make this a stable thing to use. I probably would update all of my bigger codebases using it.
Also - and thats something thats annoying me ALOT beacuse it made me spend alot of hours - the golang plugin system. I wrote an architecture to orchestrate processing and for certain reasons i wanted to implement the orchestrated "things" as plugins. But the plugin system as it is rn can only be described as the torments of hell. I messed with it for like 3 years till i recently dropped the plugin functionality and added the stuff directly. Plugins are a very powerfull thing and a good plugin system could be a great thing, but in its current state i would recommend noone to touch it.
These are just two points, i could list some more but the point i want to get to is : there are real things you can critizize instead of things that you create yourself or that are language design decision that you just dont like. Im not sure if such articles are the rage of someone who just is bored or its ragebait to make people read it. Either way its not helping anyone.
:Two types of nil
Other commenters have. I have. Not everyone will. Doesn't make it good.
:append with no defined ownership
I've seen it. Of course one can just "not do that", but wouldn't it be nice if it were syntactically prevented?
:It’s not portable ("just Unix")
I also only work on Unix systems. But if you only work on amd64 Linux, then portability is not a concern. Supporting BSD and Linux is where I encounter this mess.
:All hope is lost
All hope is lost specifically on the idea of not needing to write exception safe code. If panics did always crash the problem, then that'd be fine. But no coding standard can save you from the standard library, so yes, all hope about being able to pretend panic exits the problem, is lost.
You don't need to read my blog posts. Looking forward to reading your, much better, critique.
I say switching to Go is like a different kind of Zen. It takes time, to settle in and get in the flow of Go... Unlike the others, the LSP is fast, the developer, not so much. Once you've lost all will to live you become quite proficient at it. /s
I can still check out the code to any of them, open it and it'll look the same as modern code. I can also compile all of them with the latest compiler (1.25?) and it'll just work.
No need to investigate 5 years of package manager changes and new frameworks.
ISTG if I get downvoted for sharing my opinion I will give up on life.
I don't really care if you want that. Everyone should know that that's just the way slices work. Nothing more nothing less.
I really don't give a damn about that, i just know how slices behave, because I learned the language. That's what you should do when you are programming with it (professionally)
Just like every PHP coder should know that the ternary operator associativity is backwards compared to every other language.
If you code in a language, then you should know what's bad about that language. That doesn't make those aspects not bad.
If that's your argument then there are no bad design decisions for any language.
It's a shame because it is just as effective as pissing in the wind.
Of course, by your reasoning this also means you yourself have designed a language.
I'll leave out repeating your colorful language if you haven't done any of these things.
I really don’t like your logic. I’m not a Michelin chef, but I’m qualified to say that a restaurant ruined my dessert. While I probably couldn’t make a crème brûlée any better than theirs, I can still tell that they screwed it up compared to their competitor next door.
For example, I love Python, but it’s going to be inherently slow in places because `sum(list)` has to check the type of every single item to see what __add__ function to call. Doesn’t matter if they’re all integers; there’s no way to prove to the interpreter that a string couldn’t have sneaked in there, so the interpreter has to check each and every time.
See? I’ve never written a language, let alone one as popular as Python, but I’m still qualified to point out its shortcomings compared to other languages.