This article makes a lot of great points about the shortcomings of Go. I don’t think explicit error handling is one of them however. I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up. From a technical standpoint that isn’t necessarily a good argument, but from a pragmatic standpoint and decades of experience… well I will take explicit error handling which happens exactly where the errors occur every day. You can argue that Rust does it in a more elegant way, and I prefer it for personal projects. For big projects with a lot of developers of various skill level joining and leaving I think Go’s philosophy is one of the sanest approaches to error handling in the modern world.
Staying in that lane. In my part of the world Go is seeing adoption that no other “new” language has exactly because of its simplicity. It’s not the best language, but it’s often the best general purpose language because it has a lot of build in opinions which protect you from yourself.
There are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum type.
No stacktraces and error wrapping forces you to not only invent unique error messages. You must also conceive a unique wrapping message at every call-site so that you can grep the error message and approximate a stacktrace.
The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language, and the awkward variable initialization rules, make it so that you use the wrong `err` var at some point. E.g. if you want to reassign the result to an existing var, suddenly you have to declare `var err error`, and if `err` already exists then you have to reuse it.
There should be an enum type in go, or instead of the bizarre "return tuple" mechanics exclusive for errors, they should have added a better syntax sugar for errors like rust's `?` sugar. Instead we have something extremely tedious and quite error prone.
> it has a lot of build in opinions which protect you from yourself
It does have opinions, but too often they seem to be there to protect the language from being criticized. Sadly, this works, as marketing (lying) is an important factor towards making a PL popular in today's market.
The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface.
IMO it's okay to make behaviour-exceptions specifically for error handling. Rust for example doesn't really have builtin behaviour exceptions specifically for errors, they're generic to sumtypes and just happen to work well for errors. But then in practice you must resort to thiserror or anyhow helper crates to deal with errors in anything but tiny programs.
If you do make behaviour exceptions for error handling, be honest and upfront about it. Don't say "errors are just values so just use them like regular vars" in docs, if then there are several huge exceptions (tuple-returns and breaking a core tenet). If you make exceptions then you might as well do them right, instead of half-assing them. I believe zig does it right, but I haven't gotten around to try it.
> The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface.
To be fair, that expression came from a blogger who often wrote about Go. It is not a tenet held by the Go project. In fact, Rob Pike has made clear that he considers it to be misguided advice. It is only violated in the same way returning Result in Rust violates the assertion I made up for this comment: Do not return Result.
https://github.com/pkg/errors provides stack traces support. This unfortunately did not get included in the Go1.13 release when error wrapping was introduced.
> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language
MRV, go does not have tuples.
Go is not the only language with MRV (as a special case) and they’re not necessarily bad, iirc Common Lisp uses them as auxiliary data channels and has a whole host of functions to manipulate and refit them.
Go is singularly incompetent at MRVs though, in the sense that only syntax and builtin functions get access to variable arity (e.g. if you access a map entry you can either get one return which is a value, or two which are the value and whether the key is/was in the map). So MRVs mostly end up being worse tuples infecting everything (e.g. iterators needing Iter and Iter2 because you can’t just yield tuples to for loops).
Rust and Go's lack of stack traces are basically equivalent in that you need to call an additional function to add the stack context to the error result. For go you use fmt.Errorf, in Rust you use .context from anyhow (bad practice in many contexts IMO) or .inspect_err + log. It's rather unfortunate that neither has an easy way of capturing a line number + file easily and appending it to the context. Go could easily do it, I think. Oh well.
I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do.
IMO Go should add really basic tagged unions and rework the stdlib to use them, but such a change would probably necessitate a v2.
RE: Golang v2, they clearly said they will not do it and will double down on backwards compatibility with exceptions powered by env vars and/or CLI switches.
> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language
Go functions also accept a tuple on input. While theoretically you could pass an error, or a pointer to an error for assignment, it is a stretch to claim it is for errors.
I prefer the structure of Rust errors as it’s fully typed, I don’t like that you can chain them though. It’s a cool feature but it leaves you with some of the same issues exception handling does when the freedom is used “wrong”.
Checked exceptions may be implemented as a sum type. Traditional exceptions are more likely to be a single type that wraps up a context object alongside stack trace information.
Not really. Exceptions usually imply unwinding the stack, and the ability to catch at any point throughout the callstack. Result types are just 'dead' data.
These are fully equivalent in outcome, though often not low-level implementation. You can use try...catch (called panic...recover in Go) to pack a normal and abnormal return case into the equivalent of a Result<> type. Or just pass an abnormal Result<> back to the caller to manually unwind a single "layer" of the call stack.
There is some DX similarity between checked exceptions and Result types.
Because the compiler will fail if you don't explicitly mention each possible exception.
But checked exceptions are coming out of style: They're unchecked in C#, and frameworks like Spring Boot in Java catch all checked exceptions and rethrow them as Spring Boot flavored unchecked ones.
For unchecked exceptions and Result types:
The DX is very different in one critical way:
With Results you constantly have to differentiate between error and ok states, before you proceed. With unchecked exceptions you generally assume you're always in an ok state. It's equivalent to wrapping your whole function body in 'try { ... } catch (Exception e)'. And you can get that with Result types in Rust by using '?' and not worry about doing something half-way.
Having programmed for over 30 years, including nearly a decade of C#, I would say exceptions are one of the worst ideas in all of programming.
They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
Even junior engineers have a trivial time debugging most go errors, while even experienced principles struggle with figuring out the true cause of a Java exception.
Well, I've also programmed for over thirty years and I wouldn't use a language without exceptions and even wrote a whole essay defending that position:
> Even junior engineers have a trivial time debugging most go errors
Not my experience at all. I had to do this once. An HTTP request to a production server was yielding a 400 Bad Request with no useful information for what was bad about it. No problem, I'll check the logs and look at the source code. Useless: the server was written in Go and the logs had no information about where the error was originating. It was just getting propagated up via return codes and not logged properly. It ended up being faster to blackbox reverse engineer the server. In a language with exceptions there'd have been a stack trace that pinpointed exactly where the error originated, the story of how it was handled, and the story of how the program got there.
Diagnosing errors given stack traces is very easy. I've regularly diagnosed subtle bugs given just logs+stack trace and nothing else. I've also had to do the same for platforms that only have error codes that aren't Go (like Windows). It's much, much harder.
> Diagnosing errors given stack traces is very easy.
This is the most important aspect of exceptions in my view.
The line that threw the exception isn't even the part of a stack trace that I find most interesting. The part that is most valuable to me when working on complex production systems are all of the call sites leading up to that point.
I remember in my junior years I wasn't a big fan of exceptions. A stack trace would make my eyes glaze over. I would try/catch at really deep levels of abstraction and try to suppress errors too early. It took me a solid ~5 years before I was like "yes, exceptions are good and here's why". I think a lot of this boils down to experience and suffering the consequences of bad design enough times.
Exception usability is definitely an area that needs work. If you work support for a dev platform for a while, it's a really common experience that people will post questions with a copy/pasted stack trace where the message+stack actually answers their question. You can just copy/paste parts back to them and they're happy. There's too much information and not enough tools to digest/simplify them and people get overwhelmed.
Still, better to have too much data than too little.
> Useless: the server was written in Go and the logs had no information about where the error was originating. [...] In a language with exceptions there'd have been a stack trace that pinpointed exactly where the error originated
Go has support for exceptions, not to mention providing runtime access to stack trace information in general. They are most definitely there if your application requirements necessitate, which it seems yours did. Unfortunately, language support only helps if your developers actually speak the language. Go in particular seems especially prone to attracting developers who are familiar with other languages, who insist on continuing to try to write code in those other languages without consideration for how this one might be different and then blame the technology when things don't work out...
I can't agree with you about C++ exceptions being worse than useless. Exceptional C++ is worth it. Safety isn't that hard with RAII and ScopeGuard..
In your map example, just add a scope guard that removes the just-added element using the returned iterator if the rest of the procedure doesn't succeed. It's no different in Java.
Many people against Go's error handling do not advocate for exceptions, but for a combination of an Either/Result type (for recoverable errors) and fully aborting (for unrecoverable errors).
Abort on the other hand is used WAY to liberally in Rust.
How I hate it, that every second function call can break my program when it's clearly not a "halt the world, it's totally unrecoverable that the user sent us nonsense" type.
If a Rust function can panic, there's generally a non-panicking alternative. For example, `Vec` indexing has `vec[n]` as the panicking version and `vec.get(n)` as the version that can return `None` when there's nothing at that index.
I do wish this is something Rust had done better though - the panicking versions often look more attractive and obvious to developers, and that's the wrong way round. Vec indexing, IMO, should return Option<T>.
One of my dream projects is creating a Rust stdlib based solely on panics for error handling. Sure, it'd be incompatible with everything, but that might be a feature, not a bug.
I’ve seen this argument, but if you look at real golang code and examples, it’s just a bunch of “if err <> nill” copy pasta on every line. It’s true that handling errors is painstaking, but nothing about golang makes that problem easier. It ends up being a manual, poor-man’s stack-trace with no real advantage over an automatically generated one like in Python.
Which could be solved in one swipe by adding a Result<T, Error> sum type, and a ? operator to the language. This is more a self-inflicted limitation of Go, then a general indictment of explicit error handling.
Exception and explicit on-the-spot handling are not the only two ways to handle failing processes. Optional/result types wrapping the are a clean way to let devs handle errors, for instance, and chaining operations on them without handling errors at every step is pretty ergonomic.
Rust's error handling evolution is hilarious. In the beginning, the language designers threw out exceptions --- mostly, I think, because Go was fashionable at the time. Then, slowly, Rust evolved various forms of syntactic sugar that transformed its explicit error returns into something reminiscent of exceptions.
Once every return is a Result, every call a ?, and every error a yeet, what's the difference between your program and one with exceptions except the Result program being syntactically noisy and full of footguns?
Better for a language to be exceptional from the start. Most code can fail, so fallibility should be the default. The proper response to failure is usually propagating it up the stack, so that should be the default too.
> what's the difference between your program and one with exceptions
Because errors as values are explicit. You're not forced to use ? everywhere; you can still process errors however you like, or return them directly to the calling function so they deal with it. They're not separate control flow like exceptions, and they're not a mess like Go's.
No, because you end up with a function coloring problem that way. A function that returns something other than Result has to either call only infallible code or panic on error, and since something can go wrong in most code, the whole codebase converges as time goes to infinity on having Result everywhere.
Yeah, yeah, you can say it's explicit and you can handle it how you want and so on, but the overall effect is just a noisy spelling of exceptions with more runtime overhead and fewer features.
As almost always, we programmers / software developers / engineers, forget to state our assumptions.
In closed-world, system-software, or low-level software you want to have your kind of knowledge about everything you call. Even more: can it block?
In open-world, business-software, or high-level software it is often impossible or impractical to know all the ways in which a function or method can fail. What you need then, is a broad classification of errors or exception in the following two dimensions: 1. transient or permanent, 2. domain or technical. Those four categories are most of the time enough to know whether to return a 4xx or 5xx error or to retry in a moment or to write something into a log where a human will find it. Here, unchecked exceptions are hugely beneficial. Coincidentally, that is the domain of most Java software.
Of course, these two groups of software systems are not distinct, there is a grey area in the middle.
> A function that returns something other than Result has to either call only infallible code or panic on error
...Or solve the problem. A library function that can be a source of issues and can't fix these issues locally should simply not be returning something that is not a result in that paradigm.
> since something can go wrong in most code
That is not my experience. Separating e.g. business logic which can be described cleanly and e.g. API calls which don't is a clear improvement of a codebase.
> the whole codebase converges as time goes to infinity on having Result everywhere.
As I said previously, it is pretty easy to pipe a result value into a function that requires a non-result as input. This means your pure functions don't need to be colored.
> Or solve the problem. A library function that can be a source of issues and can't fix these issues locally should simply not be returning something that is not a result in that paradigm.
People "solve" this problem by swallowing errors (if you're lucky, logging them) or by just panicking. It's the same problem that checked exceptions in Java have: the error type being part of the signature constrains implementation flexibility.
In my experience an unwrap, expect or panicking function is a direct comment in code review and won’t be merged without a reason explaining why panicking is acceptable.
One practical benefit of Rust’s approach that hasn’t been emphasised enough yet is the consequences of Option<T> and Result<T, E> being just values, same as anything else.
It means you can use things like result.map_err(|e| …) to transform an error from one type to another. (Though if there’s a suitable From conversion, and you’re going to return it, you can just write ?.)
It means you can use option.ok_or(error) or option.ok_or_else(|| error) to convert a Some(T) into an Ok(T) and a None into an Err(E).
It means you can .collect() an iterator of Result<T, E> into a Vec<Result<T, E>>, or (one of the more spectacular examples) a Result<Vec<T>, E> which is either Ok(items) or Err(first_error).
It’s rather like I found expression-orientation, when I came to Rust from Python: at first I thought it a gimmick that didn’t actually change much, just let you omit the `return` keyword or so. But now, I’m always disappointed when I work in Python or JavaScript because statement-orientation is so limiting, and so much worse.¹ Similarly, from the outside you might not see the differences between exceptions and Rust-style Result-and-? handling, but I assure you, if you lean into it, it’s hard to go back.
—⁂—
¹ I still kinda like Python, but it really painted itself into a corner, and I’ve become convinced that it chose the wrong corner in various important ways, ways that made total sense at the time, but our understanding of programming and software engineering has improved and no new general-purpose language should make such choices any more. It’s local-maximum sort of stuff.
Exceptions are values, but normal-value-or-exception (which is what Result<T, E> is) isn’t a value. Review my remarks about map_err, ok_or, &c. with the understanding that Result is handling both branches of the control flow, and you can work with both together, and you might be able to see it a bit more. Try looking at real code bases using these things, with things like heavily method-chained APIs (popular in JS, but exceptions ruin the entire thing, so such APIs in JS tend to just drop errors!). And try to imagine how the collect() forms I described could work, in an exceptions world: it can’t, elegantly; not in the slightest.
Perhaps this also might be clearer: the fuss is not about the errors themselves being values, but about the entire error handling system being just values.
The '?' operator is the opposite of a footgun. The whole point of it is to be very explicit that the function call can potentially fail, in which case the error is propagated back to the caller. You can always choose to do something different by using Rust's extensive facilities for handling "Result" types instead of, or in addition to, using '?'.
• they may propagate automatically from any point in code, potentially breaking atomicity invariants and preventing forward progress, and have to be caught to be transformed or wrapped – Result requires an explicit operator for propagation and enables restoring invariants and transforming the error before it is propagated.
• they are an implicit side-channel treated in the type system like an afterthought and at best opt-out (e.g. "noexcept") – Result is opt-in, visible in the return type, and a regular type like any other, so improvements to type system machinery apply to Result automatically.
• try…catch is a non-expression statement, which means errors often cannot be pinpointed to a particular sub-expression – Result is a value like any other, and can be manipulated by match expressions in the exact place you obtain it.
Sure, if you syntactically transform code in an exception-based language into Rust you won’t see a difference – but the point is to avoid structuring the code that way in the first place.
> they may propagate automatically from any point in code, potentially breaking atomicity invariants and preventing forward progress
A failure can propagate in the same circumstances in a Rust program. First, Rust has panics, which are exceptions. Second, if any function you call returns Result, and if propagate any error to your caller with ?, you have the same from-anywhere control flow you're complaining about above.
Programmers who can't maintain invariants in exceptional code can't maintain them at all.
> try…catch is a non-expression statement,
That's a language design choice. Some languages, like Common Lisp, have a catch that's also an expression. So what?
> they are an implicit side-channel treated in the type system like an afterthought an
Non-specific criticism. If your complaint is that exceptions don't appear in function signatures, you can design a language in which they do. The mechanism is called "checked exceptions"
Amazing to me that the same people will laud Result because it lifts errors into signatures in Rust but hate checked exceptions because they lift errors into signatures in Java.
Besides, in the real world, junior Rust programmers (and some senior ones who should be ashamed of themselves) just .unwrap().unwrap().unwrap(). I can abort on error in C too, LOL.
Honest question: syntactically noisy as opposed to what? In the context of this post, which is a critique of Go as a programming language, for me this is orders of magnitude better than the "if err != nil {" approach of Go.
I'm not sure why you bring up Rust here, plenty of libs/languages use the Result pattern.
Your explanation of what bothers you with results seems to be focused on one specific way of handling the result, and not very clear on what the issue is exactly.
> what's the difference between your program and one with exceptions
Sometimes, in a language where performance matters, you want an error to be handled as an exception, there's nothing wrong with having that option.
In other languages (e.g. Elm), using the same Result pattern would not give you that option, and force you to resolve the failure without ending the program, because the language's design goals are different (i.e. avoiding in-browser app crash is more important than performance).
> syntactically noisy
Yeah setting up semantics to make users aware of the potential failure and giving them options to solve them requires some syntax.
In the context of a discussion about golang, which also requires a specific pattern of code to explicitly handle failures, I'm not sure what's your point here.
> full of footguns
I fail to see where there's a footgun here? Result forces you to acknowledge errors, which Go doesn't. That's the opposite of a footgun.
> Once every return is a Result, every call a ?, and every error a yeet
The Try operator (`?`) is just a syntax sugar for return. You are free to ignore it. Just write the nested return. People like it for succinctness.
Yeet? I don't understand, do you mean the unstable operator? Rust doesn't have errors either.
> what's the difference between your program and one with exceptions except the Result program being syntactically noisy and full of footguns?
Exceptions keep the stacktrace, and have to be caught. They behave similar to panics. If panics were heavy and could be caught.
Rust errors aren't caught, they must be dealt with in whatever method invokes them. Try operator by being noisy, tells you - "Hey, you're potentially returning here". That's a feature. Having many return in method can both be a smell, or it could be fine. I can find what lines potentially return (by searching for `?`).
An exception can be mostly ignored, until it bubbles up god knows where. THAT IS A HUGE FOOTGUN. In Java/C# every line in your program becomes a quiet return. You can't find what line returns because EVERY LINE CAN.
When I took the compiler course at university, the professor would have a new coursework theme every year, and the year I took the course, the coursework compiler was exception-oriented. So exceptions were the only control flow mechanism besides function calls. If/else, while, return were all variations of throw.
To me this proved that there's nothing inherently wrong about exceptions.
It's how you structure your code and the implied assumptions you share with your colleagues.
Some people are way too optimistic about their program will actually do.
In OCaml, that's actually a common use for exceptions, as they let you unwind extremely quickly and carry a result (helped by the fact that OCaml doesn't have finally or destructors).
I agree about implicit exceptions, but I think that there is a sweet spot with explicit exceptions like Swift (and maybe Java): where you cannot not-handle one, it is part of a function's signature, and the syntax is still compact enough that it does not hurt readability.
Is “goto” just used to mean “bad and evil” here? Because exceptions are not a goto anymore than a return is a goto. The problem with goto is it can jump to any arbitrary place in your code. Exceptions will only go to catch-blocks up the call stack, which presumably you have written on purpose.
Hard disagree. Exceptions are actually good. They make code clear and errors hard to ignore. I've written a ton of code over decades in both exceptional and explicit-error languages and I'll take the former every day. There's no function color problem. No syntactic pollution of logic with repetitive error propagation tokens.
Also, exception systems usually come with built in stack trace support, "this error caused by this other error" support, debugger integration ("break the first time something goes wrong"), and tons of other useful features.
(Common Lisp conditions are even better, but you can't have everything.)
You can't just wave the word "goto" around as if it were self-evident that nonlocal flow control is bad. It isn't.
> And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
That's not been my experience at all. Writing exception safe code is a matter of using your language's TWR/disposable/RAII/etc. facility. A programmer who can't get this right is going to bungle explicit error handling too.
Oh, and sum types? Have you read any real world Rust code? Junior developers just add unwrap() until things compile. The result is not only syntactic clutter, but also a program that just panics, which is throwing an exception, the first time something goes wrong.
Many junior developers struggle with error handling in general. They'll ignore error codes. They'll unwrap sum types. They might... well, they'll propagate exceptions non-fat ally, because that's the syntactic default, and that's usually the right thing. We have to design languages with misuse in mind.
> Have you read any real world Rust code? Junior developers just add unwrap() until things compile.
If you really don't like unwrap[1], you can enable a linter warning that will let you know about its uses to flag it during code review. You know exactly where they are and when they happen. Exceptions are hidden control flow, so you rely on documentation to know when a function throws.
> Writing exception safe code is a matter of using your language's TWR/disposable/RAII/etc. facility. A programmer who can't get this right is going to bungle explicit error handling too.
Rust has RAII, so you don't have to worry about clean-up when returning errors. This is a Go problem, not Rust.
Bah, no, I hated that you had to wrap basically every code block in a try/catch in Java, because the underlying lib could change and suddenly throw a Runtime-Exception.
At the same time Checked Exceptions were a nightmare as well, because suddenly they were part of the contract, even though maybe wrong later.
> the underlying lib could change and suddenly throw a Runtime-Exception.
And what would you do in that case? Since this is a future change your existing code presumably wouldn't know what else to do but throw its own exception, so why not just let that one propagate?
Not having checked exceptions is a huge problem, because then you never know when something might throw and what it might through, and in the .NET world the documentation on that is pretty awful and absolutely incomplete.
But then over in Java world, your checked exception paradise (which it of course isn't because the syntax and toolkit for managing the things is so clunky) is easily broken by the number of unchecked exceptions which could be thrown from anything at any time and break your code in unexpected and exciting ways, so not only do you have to deal with that system you also don't get any assurance that it's even worth doing.
But this doesn't actually mean checked exceptions are a bad idea, it means that Java didn't implement them very well (largely because it also has unchecked exceptions, and NullPointerException is unchecked because otherwise the burden of handling it would be hideous, but that comes down to reference types being nullable by default, which is a whole other barrel of pain they didn't have to do, and oh look, Go did the same thing wooo).
> in the .NET world the documentation on that is pretty awful and absolutely incomplete.
Depends on the area you look at. Language documentation is pretty good and so is documentation for the standard library itself. Documentation for the frameworks can be hit or miss. EF Core is pretty well documented and it’s easy to find what to look for usually. GUI frameworks are more of a learning curve however.
FWIW many in Java community consider checked exceptions to be a mistake. While I don’t find writing code that has many failure modes particularly fun with exception handling - Rust perfected the solution to this (and the Go way is visually abrasive, no thanks), I don’t think it’s particularly egregious either - Try pattern is pretty popular and idiomatic to use or implement, and business code often uses its own Result abstractions - switch expressions are pretty good at handling these. Personally, I’d write such code in F# instead which is a recent discovery I can’t believe so few know how good it is.
What does make exceptions bad in my opinion (and shared by Go developers?) is a few things:
1. Exceptions are expensive (at least in Java / C#), as they generate a stack trace every time. Which is fine for actually exceptional situations, the equivalent of `panic()` in Go, but:
2. Exceptions are thrown for situations that are not exceptional, e.g. files that don't exist, database rows that don't exist, etc. Those are simple business logic cases. The workaround is defensive coding, check if the file exists first, check if the row exists? that kind of thing.
3. The inconsistency between checked and unchecked exceptions.
4. Distance - but this is developer / implementation specific - between calling a function that can throw an error and handling it.
But #2 is the big one I think. Go's error handling is one solution, but if it's about correct code, then more functional languages that use the Either pattern or whatever it's called formally are even better. Go's approach is the more / most pragmatic of the options.
> e.g. files that don't exist, database rows that don't exist, etc. [...] The workaround is defensive coding, check if the file exists first, check if the row exists?
Ugh NO. Please don't. You should never "check if the file exists first". It can stop existing between your check and your later attempt at opening the file (the same with database rows). That can even lead to security issues. The name for that kind of programming mistake, as a vulnerability class, is TOCTOU (time-of-check to time-of-use).
The correct way is always to try to do the operation in a single step, and handle the "does not exist" error return, be it a traditional error return (negative result with errno as ENOENT), a sum type (either the result or an error), or an exception.
Without fail, every single person I’ve seen rave about go’s error handling compares it only to exceptions as if that’s the only alternative.
On the flip side I have yet to find a person who’s familiar with sum types (e.g., Maybe, Option, Result) that finds the golang approach even remotely acceptable.
I think it’s because Go is an alternative to Java and C# more so than an alternative to Rust. It is for me at least. As I said, Rust isn’t seeing any form of real world adoption in my region while Go is. Go isn’t replacing C/C++ or even Python though, it’s replacing Typescript, C# and Java. Now, there are a lot of good reasons as to why Go shouldn’t be doing that, a lot of them listed in the article, but that’s still what is happening.
As I pointed out I think Rust does it better with its types error handling. That isn’t too relevant for me though as Rust will probably never seem any form of adoption in my part of the world. I think Zig may have a better chance considering how interoperable it is with C, but around here the C++ folks are simply sticking with C++.
> Now, there are a lot of good reasons as to why Go shouldn’t be doing that
I disagree. Typescript, C# and Java are terrible languages (as are Python/Ruby/etc. in other ways). Golang is bad by OP's standards but there's nothing wrong with it gaining ground on those languages.
Besides it's also easier to convert a codebase to Rust from Golang than Typescript or C#/Java.
Rust and C# have far more overlap than Go could ever hope for. Go is limited (and convoluted sometimes due to "solutions" devised to cope with it) so it is easily expressible in languages with better type systems and concurrency primitives.
I dislike sum-type based error handling. It is annoying syntactically and only really doable with lots of high-level combinators, which in turn hinder debuggability.
Have you tried the approach that Zig has, or the approach that Rust has? They are easy to debug and do not use any crazy stuff, just simple syntax like `try x()` (Zig) or `x()?` (Rust)
You hit the main gripe I have with Go, its types system is so basic. I get people raving type-correctness of Go when they come from Python but the type system in Go is simply pre-historic by modern day standards.
I feel that the future for Python people who want type safety will eventually be TypeScript on nodejs. Go was intended as an alternative to C++. It seems that in reaction to the ungodly complexity of C++, the creators wanted to avoid adding language features as hard as possible. If the user could work around it with a little extra verbosity, it'd be ok. I feel they removed too much and maybe not the right things.
Do you have a pydantic equivalent in go? Also modern typing in python is starting to be OK to be honest (well, if you consider typescript typing OK), so it isn't really a knock on Go :)
Which, sadly, is still the case of too many dependencies.
While I much prefer Python as a language, Go wins against Python by having a fresher ecosystem, with a higher baseline for type safety. Still pretty low with respect to Rust or mypy/pyright with highest settings, but much better than any of the Python frameworks I've had to deal with.
I don't agree that that's what it's marketed towards, but it was designed with those in mind. That said, experienced developers can enjoy it too, as code is just a means to an end and code complexity or cleverness does not make for good software in the broader sense of the word.
It's a Google solution to Google scale problems, e.g. codebases with millions of lines of code worked on by thousands of developers. Problems that few people that have an Opinion on Go will ever encounter.
What do you mean by this? WebAssembly is a low level bytecode which only defines low level types. WebAssembly doesn't "have" types any more than x86 "has" types right? Or have I missed something?
this part of error handling is pure religion. it goes even against one of the most basic go tenents. that code should be easy to read not write.
Try reading and understanding the logic of a particular method where 75% of the lines are error noise and only 25% are the ones you need to understand what the method does.
yes it's noise because whenever read a codebase for the first time you are never interested on the the error edge case. first glance readability needs to tell you what you are trying to accomplish and only after what you are doing to make sure that is correct.
on this point go's error handling is a massive fail.
Notice that I'm not saying explicit error handling is bad. I'm saying the insistence that error handling needs to be implemented inline interleaved with the happy path is the problem.
You can have explicit error handling in dedicated error handling sections
Some people didn't like the "try" keyword it reminded them too much of exceptions, some people didn't like that they couldnt see a return inline (which was the purpose of the proposal in the first place).
it's not that there are no solutions. the main problem is the go team's insistence to have "one true way" (tm) of doing something and unfortunately this gap between people who want to see every return inline and people who want to see the clean solution separate from the error handling is not something that can be bridged by technical means. the only solution is to implement both ways and lets see which one wins.
I would certainly argue against the claim that explicit error handling is far overkill.
Where I agree: It forces you to think about all of the possibilities your code might generate. (This is more of a C question than it is with other languages)
However, when abstracting blocks of code away, you don't always need to handle the error immediently or you may want to handle it down the stack.
You're giving up a lot of readability in order for the language to be particular.
> It forces you to think about all of the possibilities your code might generate.
Except it doesn't actually. You can totally just ignore it and pretend errors don't exist. Lack of sum types/Result, and pointers as poor mans optional, really hinder's Go's error handling story.
> It forces you to think about all of the possibilities your code might generate
I’ve seen way too much Go code which never even tested the err value to believe that until something like errcheck is built in to the compiler.
I do agree that this is a plus for the explicit model but that’s been a better argument for Rust in my experience since there’s better culture and tooling around actually checking errors. I’m sure there are plenty of teams doing a good job here, but it always felt like the one lesson from C they didn’t learn well enough, probably because a language created by experts working at a place with a strong culture of review doesn’t really account for the other 99% of developers.
For me, the issue with error handling is that while errors are explicitly stated, they are often poorly handled. Rarely have I seen the handling of multiple reasons for why an error might occur, along with tailored approaches to handle each case. This is something very common in older languages like Python or Java
As a regular Go user, I agree with this take. Though the tools exist, error wrapping and checking (with errors.Is and so on) is actually pretty rare in my experience.
This is down to developer style and agreements though; Go has typed errors and a set of utilities to match them [0]. Not using those is a choice, just like how in Java you can just `catch (Exception e)` after calling a dozen methods that might each throw a different exception.
Interestingly, every time (and I mean _every_ time) that I've tried to use `errors.As` on errors raised by lib code, I found out that the lib just went with "nah, I'm just going to use `errors.New` or `fmt.Errorf`", which makes the error impossible to match.
So... I'd say that this is a fumble in the design of Go.
I see a lot of people say this about exceptions, and I don't have that problem. The exception bubbles up the stack until something catches it. Ok it's a different code path, but it's a very simple one (straight up). So you either catch the exception nearby and do something specific with it, or it bubbles up to a generic "I'm sorry there was a problem please try again later" handler.
Honestly makes me wonder what I'm missing. Maybe it's because I don't deal with state much? Do the problems start to mount up when you get into writing transaction locks, rollbacks etc? But I don't see why you wouldn't have the same problems with Go's mechanism.
Hoping to gain enlightenment here.
[copied from a comment below]: They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
Maybe this is it? I prefer a "fail early and often" approach, and I tend to validate my data before I perform operations on it. I don't want to attempt recovery, I want to spew log messages and quit.
> They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
I think that’s misdirected but illustrates the emotional reasons why people develop a negative impression of the concept. Usually it means someone had bad experiences with code written in a poor culture of error handling (e.g. certain Java frameworks) and generalized it to “exceptions are bad” rather than recognizing that error handling isn’t trivial and many programmers don’t take it seriously enough, regardless of the paradigm. As a simple example, C and PHP code have had many, many security and correctness issues caused by _not_ having errors interrupt program execution where the users would have been much better off had the program simply halted on the first unhandled error.
If you write complex programs with lots of mutable shared state, yes, it’s hard to reason about error recovery but that’s misattributing the problem to the mechanism which surfaced the error rather than the fact that their program’s architecture makes it hard to rollback or recover.
> Go is seeing adoption that no other “new” language has exactly because of its simplicity
Yes - for me, the simplicity is essential. As a part-time programmer, I don't have months to spend learning C++ or Rust.
If my project needs to compile to small(-ish) standalone binaries for multiple platforms (ruling out Python, Ruby, Java, C#, etc) what simple alternative language is there? Plain C?
Basic Rust doesn’t take months to learn, especially when you’re not trying to do things like distributing crates to other people. I found the compiler to be enough more helpful than Go’s to make them roughly time-equivalent for the subset of common features, especially for a simple CLI tool.
More importantly, Rust has the notion of a result type and it is designed to be both generic and composable.
A problem I often face in Go and TypeScript code is code that ignores errors, often unintentionally. For instance, many uses of JSON.parse in TypeScript do not check for the SyntaxError that may be thrown. In Go, it is common to see patterns like
_ := foo.Bar()
// assume Bar() returns error
This pattern exists to tell the reader "I don't care if this method returns an error". It allows one to avoid returning an error, but it also stops the caller from ever being handle to the error.
Also, the position of the error matters. While the convention in the stdlib is to return errors as the final value, this isn't necessarily followed by third party code.
Similarly, errors are just an interface and there is no requirement to actually handle returned errors. Even if one wants to handle errors, it's quite awkward having to use errors.As or errors.Is to look into a (possibly wrapped) chain of errors.
The benefit of Rust's Result<T, E> is that
- position doesn't matter
- there is strong, static type checking
- the language provides operators like ? to effortlessly pass errors up the call stack, and
- the language provides pattern matching, so it's easy to exhaustively handle errors in a Result
The last two points are extremely important. It's what prevents boilerplate like
if err != nil {
return nil, err
}
and it's what allows one to write type-safe code rather than guess whether errors.As() or errors.Is() should be used to handle a returned error.
I am pretty sure if it were for the Typescript creators they would not allow exceptions in the language, but they had to work within the confines of Javascript. Heck they even refused to make exceptions part of the type-system.
It is unfortunate that many of Typescript developers still rely on throwing exceptions around (even in their own typescript code). Result types are totally doable in Typescript and you can always wrap native calls to return result types.
Why would you "check" for TypeError being thrown? Just let exceptions in general propagate until they reach one of the few places in the program that can log, display, crash, or otherwise handle an exception. No need to "check" anything at call sites.
90% of the criticism of exceptions I see comes from the bizarre and mistaken idea that every call needs to be wrapped in a try block and every possible error mentioned.
Unsure if this is the right place to ask, but this conversation inspires me this question:
Is there in practice a significant difference between try/catch and Go's "if err" ? Both seem to achieve the same purpose, though try/catch can cover a whole bunch of logic rather than a single function. Is that the only difference ?
Try/catch can bubble through multiple layers. You can decide/design where to handle the errors. If you don't `if err` in Golang, the error is skipped/forgotten, with no way to catch it higher up.
You can decide not to catch a thrown exception, it travels upwards automatically if you don't catch it.
I think that's the biggest difference.
With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards. If you do, what kind of context would the caller want from this piece of code, you can add that too.
> With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards.
Is this really all that interesting or worth the LOC spent on error handling when 99.9999% of the time in practice it’s just bubbled up?
And any “context” added is just string wrapping. Approximately nobody types golang errors in a way that lets you programmatically know what went wrong, to be able to fix it in-line.
I think I would be more empathetic to the arguments defending golang here if I’d ever worked or seen a project where people actually handled errors instead of spending 2/3 of their time writing code that just punts on any error.
I'd argue that at least checked exceptions also require a conscious decision from you. You either need to add the Exception type to your throws clause, or your catch clause.
Compared to Go, this is actually better because the type system can tell you what kind of errors to expect, instead of just "error".
> I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up.
I kind of see your point. In this very moment, it doesn't matter whether I agree. What I don't understand, though, is why (typically) people who abhor exceptions are among the fiercest defenders of garbage collection, which does add a “magic” and uncontrollable layer to object destruction.
Personally, having learned to love RAII with C++, I was shocked to discover that other languages discarded it initially and had to add it in later when they realized that their target developers are not as dummy as those choosing Golang.
Different kind of magic. Needing to account for every single line of code being able to throw an exception is very mentally taxing, whereas the existence of a GC removes mental load of needing to account for every single allocation.
How does RAII works in concurrent systems ? It seems to me you need to add compile-time object lifetime evaluation (as in rust) which so far incurs a high toll on language complexity.
The exact same way it works in Rust. C++'s RAII works the same as Rust's Drop trait. The object is released when it goes out of scope, and if it's shared (e.g. Arc), it's released when the last reference to it drops.
When I started trying to teach myself Rust, the error handling story fell apart on me very quick.
Like as soon as I wanted to try and get sensible reporting in their, suddenly we were relieving libraries, adding shims and fighting mismatched types and every article was saying the same thing: haha yeah it's kind of a problem.
I'm very, very unsold on explicit error handling compared to exceptions for practical programming. The number of things which can error in a program is far larger then those that can't.
I felt the same but after switching to anyhow and thiserror in pretty much every Rust project I work on I find it quite painless. It's not ideal to rely on crates for a core language feature but I never find myself fighting error types anymore. Have you tried those crates? Do you still hold that opinion?
You don’t need crates for it, anyhow is basically a better Box<dyn Error>, if you just want the error signal you can use that. The main thing missing from the stdlib fur this use case is I don’t think there’s anything to easily wrap / contextualise errors built in.
The problems you're describing don't exist in go. There is exactly one standard type that is used by everyone, at least in public API's, you can always just return the error to the caller, if you don't want to handle it in place. The main difference with exceptions in my practice is the fact that it's a lot easier to mess up, since it requires manual typing. This is probably my main problem with everything being as explicit as possible: it requires people to not make mistakes while performing boring manual tasks. What could possibly go wrong?
The drawback, on the other hand, is that all the Go code I've read (including the stdlib and all the dependencies of my current project) is using `fmt.Errorf` or `errors.New`, which means that you can't use `errors.As`, which means that you generally cannot handle errors at all.
I think that sort of nails it: the problem with errors as values is errors become part of the type signature and put under user control, and the user can't really be trusted with that power.
Even the simplest functions tend to have error states - i.e. floating point math can always wind up handing back NaN.
So where I end up is, the default assumption is every function is effectively of a type MightError(T)...so why even make us type this? Why not just assume it, assume the unwrap handling code, and so you basically wind up back at try-catch exception handling as a syntactic sugar for that whole system.
Rust and Go are very different and I feel people want a middle ground that just doesn't exist currently.
A garbage collected relatively simple language that compiles into a statically linked binary but has a type system similar to rust, rest types etc.
Syntactically, Gleam and Kotlin come somewhat close but not really. I like Rust but I do believe it is too complicated for many people who are capable of creating something but are not CS grads nor working as programmers. If you're only gonna use the language every once in a while you won't remember what a vtable is, how and when things are dropped etc. I understand that "the perfect language" doesn't exist but I think both Go and Rust brought amazing things to the table. I can only hope someone takes inspiration from both and creates a widely usable, simple programming language.
Kotlin is interesting as a middle ground, but I still find it much less productive than Go for most tasks, and unsuitable for tasks where you'd reach for Rust.
In practice, Kotlin is extremely complicated, and you end up spending time being clever. There are 1000 ways to do things. Operator overloading. Proxies. Properties. Companion objects. Exceptions AND result types...
The build system (assuming you use Gradle) is tantamount to torture for anyone used to "go build".
The coroutines APIs feel simultaneously more complicated and yet more restrictive than Goroutines. More structured but less flexible and more effort to use.
Access control feels awkward. There's no way to make a type package-private -- it's file-private or available to the whole module. This leads to either a larger API surface than desired, or the inability to break up complexity into multiple files.
Kotlin/Native and Kotlin/JVM really are two different beasts too.
Kotlin/JVM is mature, but then you are running on the JVM, so that cuts out a whole class of use cases you might bust out Rust for.
There is a very weak ecosystem for Kotlin/Native, and it's poorly documented. There are some scary bugs in the bug tracker.
You can't publish source-only libraries for Kotlin/Native either, so you need a complex CI setup to build binaries for every OS and arch under the sun. Or just don't publish libraries at all, which probably feeds in to the weak ecosystem...
Imho, most features of Rust that people would like to see in Go would still fit into the "concept" of Go. Just like they added generics, they could add just three things: A generic container for errors (Result), one for saner Nil handling (Optional) and a small sprinkling of syntax sugar to make these comfortable to work with (something like an elvis operator equivalent).
Go has the one big advantage that is almost solely responsible for it's success: It was created and directly used by a giant company that could afford to create amazing tooling around it and develop great opensource libraries for it. Already being in use and having libraries feel like the biggest determinants of a languages success.
(author here) in which ways does Gleam come short of that? Because I'm also looking for that middle ground and I was very curious to get a look at Gleam.
IMHO it's just that it's a beam VM language, which is a fatter runtime/ecosystem than is really needed to achieve the goal stated above can bring it's own bag of problems (but also it's own superpowers).
Also to be productive you have to utilize the rest of the erlang ecosystem, so at least some superficial knowledge in elixir & erlang is helpful for for some use-cases.
Syntactically I actually don't think it's that for off, but I dunno what GP was thinking, maybe that it leans more into functional patterns & sugar for those whereas rust/go can also be used in a procedural style. (Though at least personally I am using way more functional patterns in rust than I expected)
If they'd drastically improved their tooling then yes.
But sadly it's not that easy to create a statically liked binary in swift. The last time i did it it also included the whole runtime, and the resulting "hello world" binary was 50mb large. Annoying at least.
For years I wished they got their stuff together, but at this point I'd be very suprised. They probably have too much technical dept already due to the support of XXX edge cases Apple need for iOS/MacOS development.
Last time i tried to install ocaml on windows few months ago i failed to do so, it's a well known thing that it is not 100% supported on windows therefore it wont have the adoption that go/rust has (as it's been the case forever now)
This doesn't count because the idioms are very different from Go and Rust. And I suspect there's a high learning curve for features like Functors.
If you check the Wikipedia page for OCaml to find out where it gets used, you'll see why it's ocaML. That is, you'll notice that it's mostly a MetaLanguage, or a language for writing other languages. The same observation applies to other languages in the ML family.
> A garbage collected relatively simple language that compiles into a statically linked binary and has a [good] type system
Yeah! Pattern matching too. What are currently available languages closest to this? AFAIK, Gleam relies on a virtual machine, but otherwise seems promising.
Stretching 'currently available' a little, there's Roc lang [1]. Though still in development, you can use it for small personal projects. Another caveat is that it's a functional language, which could potentially hinder its wide adoption
C# and F# will be by far the closest. Other options lack sufficiently good type system or tooling to match either of the two.
Compile to static native binary with 'dotnet publish -p:PublishAot=true' (or add this property to .csproj to not specify on each publish). In the case of F#, you will need to use Console.* methods over 'print*' because print has unbound reflection inside for structural output on "%A" format specifier (it will work most of the time but negatively impacts binary size and causes the compiler to complain).
I can especially recommend F# as "easier more business-focused Rust alternative" because it is expression-oriented, has discriminated unions, full HM type inference and gradual typing is a joy to work with. Data analysis and domain modeling are very pleasant to do in it too.
For systems programming C# is going to be the option to use - it will give you great concurrency primitives, fast (sometimes even zero-cost) native interop, smaller than Go native binaries and a lot of low-level APIs including portable SIMD. Go is often poorly suited for these tasks or can't do them at all (at least without workarounds). There are many new high-performance libraries focused on this domain as .NET gains popularity in non-gaming communities in this area. And of course you benefit from a huge existing ecosystem and won't have to do the all the heavy lifting by yourself unlike in niche languages suggested in sibling comments.
I wonder what makes someone go such a great length to bash a language, any language. I say bashing, because even the few valid points in the post are not written in a constructive style. After all is there a language that can't be criticised?
Is the post written to make one feel better having a failed a project the language? (It's not me, it's the language) Or is it the failure to understand that not everyone thinks / works the same and what one finds unacceptably bothersome, others barely notice? Languages that do not cater for a real need would likely vanish on their own, they rarely need help.
As for Go, despite the differences compared to "more sophisticated" languages it works brilliantly for the projects I've encountered. I hope the author is not forced to work with Go though. For the rest of the community, we keep churning out services, giving our feedback to the Go team and seeing the slow evolution of the language without breaking our stuff in production.
> what makes someone go such a great length to bash ...
"""
Inherent complexity does not go away if you close your eyes.
When you choose not to care about complexity, you're merely pushing it onto other developers in your org, ops people, your customers, someone. Now they have to work around your assumptions to make sure everything keeps running smoothly.
And nowadays, I'm often that someone, and I'm tired of it.
"""
> [Go] works brilliantly for the projects I've encountered.
Of course, C, C++, PHP and JavaScript works too! Of course many many many things "work" in our world. Of course just adding one more lane works too, of course police states work too!
Yet something else would work even more brilliantly?
It's just some person's blog and they're having a rant. It's okay, it doesn't have to be that deep.
I would guess the 'why' is because OP feels like they have an opinion that they don't feel is sufficiently represented 'out there'. Indeed, as a not-a-fan-of-go, in 2022 I was confused at go's popularity because it always felt to me to have some pretty glaring shortcomings that were seemingly ignored.
Note that people don't really write big blog posts about PHP being a bad language (anymore?) because that's been done to death.
One very subjective, very irrational factor for my borderline hate for Go is that for years the Go zealots gaslighted everyone about every single part of Go.
Anything that Go did, no matter if it was the most basic implementation or if other languages already did it (better), was essential, the best and only way to solve that issue.
Anything Go did not do was superfluous and downright a conspiracy by Big Complexity to keep us unenlightened Non-Goers addicted to the Syntax Sugar: Things like sane null handling, sane error handling, less boilerplate, generics, or not creating special cases for everything (generics and tuples) instead of developing one cohesive solution.
Even now, in this thread, the strawmanning continues: Error handling is brought up, and instead of admitting the unmistakable truth that Gos error handling could be much better (s. Rust), people bring up things like JavaScript. As if anyone criticizing Go that JavaScript was the pinnacle of error handling.
Yes, I've also grown tired of this toxic mindset, as well as the whole "idiomatic Go" dogma that is very often an excuse for poor engineering practices.
Yeah, this was/is a part big part of my frustration with the ecosystem too. It set a LOT of very problematic patterns in place in the beginning, and many of them are still not unwound.
Stuff like "Go doesn't need dependency injection because it's simple". I heard that literally dozens of times. The opposite is true! It's an even bigger pain without DI because the language is so simple! DI everything or make your users suffer!
Or a personal favorite: "Go doesn't need a package manager". We see where that went, and how much insanely better it is now that we have it.
Or errors. Without wrapping. Oh boy we're gonna pay for that for decades.
There's stuff to like in the language, but the zealots (especially early ones) really did not know which parts they were.
Very common misrepresentation of any critique of Go. "You just don't get simplicity, you got them Java brainwormz"...
There are many examples in the article that point out the annoying inconsistencies in the language, those are the opposite of simplicity.
I love Rob Pike's presentations on Go, some of them were eye-opening to me. However, I just wish that the Go I see in practice would be much closer to the Go language that Go-fans describe in abstract.
Scheme is a simple language, Go just hides complexity until it blows up in the worst possible way. (Of course, most reasonable alternatives to Go are even worse from that POV. See Python, Ruby, JS etc.)
The difference, for example is: Go invents an abstraction just for one use case, and just for the standard library/runtime itself, instead of taking the time to create a universal version of that abstraction. Generics for maps and lists, and tuples for error handling.
The problem is that Go is not designed to be a simple language for it's users, it's designed to be a simple language to implement for it's maintainers.
In my opinion, a simple language should be highly consistent (have a few rules, but which are universal and consistent everywhere). Instead we have a language with weirdnesses, inconsistencies and workarounds all over the place.
A good example is type elision: it's available when declaring arrays, slices and maps, but not for structs. Allowing that would have several benefits in terms of readability, and also allow named parameters via anonymous struct arguments (which would greatly improve the design compared to the ugly workarounds that are currently used).
I wish the discourse that Go is a "simple" language would die.
Despite its veneer, once you start writing Go it quickly becomes apparent that it isn't simple. Hidden complexity and footguns are abundant (e.g., https://archive.ph/WcyF4).
It's nevertheless a useful language, and I use it quite a bit, but it's not "simple".
I have no idea why anyone would say it's not simple, it's super-simple. Learning how duck typing works with interfaces and how to use it is perhaps the only hurdle. In my experience, only certain BASIC dialects like VisualBasic are simpler.
I think the sticking point is what people mean when they say simple. To me, and likely to many saying Go isn't simple, simple is not a synonym for easy.
Go is easy, but it is not simple. For example, solving the problem of generics in a generic way from the start so the same problem can be addressed in the same way everywhere, would be simple, but maybe not (as) easy. Contrast that to giving the runtime/standard library a special exception with maps and lists. That's easy, but not simple. People used to literally use code generators or weird UTF-8 characters to fake generics, that's not remotely simple.
This 100%, I was just about to type a long rant up about this. There are so many weird parts of the language that took me forever to grasp, and in many cases, I still don't have an intuitive grasp of things.
And plenty of other examples that aren't in that article:
- You have a struct with an embedded interface. Does the outer struct satisfy the embedded interface? And can I type assert the outer struct into whatever embedded struct is fulfilling the inner interface?
- When should I pass by value and when should I pass by reference? Like I generally know when to choose which, but do I really know without performing a benchmark? And what about arrays? Should I store pointers in them? But it also seems that people just don't care and just randomly roll the dice on when to return a pointer or a value?
- Shorthand variable declaration. How does it work when you shorthand declare two variables but one of them already exists?
Don't both answering the questions, that's not the problem. The problem is that it's just not intuitive enough such that I'm confident I know the correct answer.
I am not going to answer the questions, but this is a very strange complaint, to be honest.
For example, passing by value/passing by reference is something covered immediately in the Go FAQ document once and for all. Everything is passed by value in Go, that is it. There should be no confusion at all. If you spend 15 minutes reading Russ Cox's post on the internals of the most common data types, you will also understand what data structures have implicit pointers under the hood.
Well yes obviously I know everything is passed by value, just like in literally every other popular language. I'm talking about the difference between pointer parameters/receivers vs value parameters/receivers.
But that's the thing right? Like I come from Java. In Java, we have objects. They are pointers. That's it. You don't get to decide on whether you want a pointer or a value (I guess primitives are an exception lol). But it was so simple!
And same in JavaScript. Everything is a pointer except primitives. That's it. End of story.
And I have written Rust too, and while the situation is definitely more complicated there, the guidance is extremely simple and straightforward: If the struct implements Copy, then it is very cheap to copy and you should pass by value. Otherwise, you should pass by pointer/reference.
And meanwhile in Go, I just see pointers and values being used seemingly interchangeably, and seemingly at random.
> but do I really know without performing a benchmark?
Not really. But that’s one of Rob Pikes rules [1], I think the intention is to write whatever is simplest and optimize later. The programmer doesn’t need to remember 100 rules about how memory is allocated in different situations.
I mean it's a great idea, and I fully agree that I do not want to worry about memory allocation. So then why is `make` a thing? And why is `new` a thing? And why can't I take an address to a primitive/literal? And yet I can still take an address to a struct initialization? And why can't I take an address to anything that's returned by a function?
"Everyone can read Go code and understand what happens."
There seems to be a difference between "easy to read" and "understand what happens" - or what happens on what level. The challenge is that there is a tradeoff between the two. Assembler is too low to understand what "really" happens, on the other hand Haskell for example with Monad stacks is again very easy to read + understand what happens "most of the time", but hard to understand all the abstracted away side effects.
In Haskell with
add 3 5
everything can happen beside what you see.
In assembler
ld a, 3
add a, 5
nothing happens except these two instructions.
The tradeoff is how much you want to be explicit, with the downside of creating too much noise, and how much you want to abstract away, with the downside of magic happening somewhere.
Go is "simple" insofar as you're doing simple things. If you've tried writing a KV store or database (as I have) you'll quickly find yourself wanting slightly more modern language features.
Feel free to take a look at any more complicated GoLang code (k8s, gorm, etc) and you'll see that the tool/library you're depending on requires a veritable rats nest of bad practices to work around the Go's inherent limitations.
My opinion on Kubernetes as a yardstick example of Go is this:
This was one of the first large systems developed in Go, not too long after the language hit version 1.0 (ca. 12 years ago). What constituted good Go style and package architecture were not well known at that time even. Given that and hindsight being 20:20, I could easily imagine the internal architecture (not even the public surface) for Kubernetes being a lot different and simpler. The package architecture alone makes me cringe. As a hypothetical counterpoint, I wonder what Kubernetes would have looked like had Dave Cheney (https://dave.cheney.net/) and Rob Pike built it in that era. I think that would have been a better yard stick.
I have a unique appreciation for this first large systems perspective as I co-designed an adjacent product in the cloud native ecosystem at that time (Prometheus). There was no good example to follow when it came to large program structure and design in Go at that time. We were all figuring that out for ourselves — organically.
I think this point about organic evolution of the architecture is important to call out explicitly, because developers often look at an existing structure and essentially mimic it with their additions, changes, and refactors irrespective of whether the structure was correct for the problem it was trying to solve or even good. Given that reality, is it really any wonder that one of the first major pieces of software ended up being this metaphorical mess? And the truth is that's not a language-specific problem: it could have happened with any new language. And taking a legacy system of this age and refactoring is difficult from a social perspective, ignoring the gradient/cost of refactoring that is language-specific. You'll have a lot of people who will oppose structural change just because, so probably a significant refactoring to achieve these goals is just not in the cards.
Every time I read a critique of Go, I feel the same way: I'm still going to continue to use it anyways. The reason why I am going to continue to use it anyways is because while I understand that it has plenty of easily documented issues in theory (and that people regularly do actually run into in practice), I still find that it is one of the better programming languages in practice anyways. Some of the things that people commonly list as shortcomings I don't agree with (I like the explicit error handling everywhere) and for other things... I often agree, but it doesn't bother me much more than the shortcomings of other programming languages, of which there is certainly no shortage.
I guess I feel bad for people who are particularly sensitive to the areas that Go does not succeed in, because they are probably going to be complaining about it for the rest of their lives. The truth is though, I don't use a bunch of rationale about what is the best programming language to choose which one to work on for a project, I choose a language that I feel works well for me, that I feel I can consistently write good software in, that feels, well, nice to work in. Being a person that values correctness, I do sort of wish that language could be something more like Rust, but for now, it's just not, though it's not like I hate Rust, it's just not what I reach for in a pinch.
Enough has been written about how terrible Go is by this point. At least now I know what it's like to have been a fan of PHP a few years ago! (That's an exaggeration, but it's not that big of one in my opinion.)
> I guess I feel bad for people who are particularly sensitive to the areas that Go does not succeed in, because they are probably going to be complaining about it for the rest of their lives.
Well, that’s a stellar
endorsement of the article, because that’s literally the point they’re making.
You’ll use go.
…and then regret it.
…but by then it’ll be too late, and you’re stuck with it.
I think the author makes a compelling argument, which is very difficult to counter, that it is a bad choice and you will regret having it in production in many of the explicitly listed cases, and in many professional situations where companies that are not technically competent use unsuitable tech.
There's no tool boring enough to prevent any chance of regret. At the end of the day, it's really, really difficult to anticipate where your pain points will actually wind up in the real world. In practice, I've had lots of really good production success with Go and not too much heartache about the choice. Since adopting it personally (in around 2014) almost every company I've gone to work since has used Go in some capacity and that capacity was usually growing because it was working for them.
Will you regret choosing Go? Maybe. Or, maybe not.
If anyone tells you C is "boring", that's just plain and simple bullshit. C gives you undefined behavior, buggy compilers (yes even for simple C code, MSVC is especially bad at plain C but there are other offenders) and the world's worst standard library for manipulating strings. Using C in embedded development is probably OK, even if you have to suffer with whatever crappy vendor compiler you are stuck with, but that's only considering the pretty severe limitations that very resource-constrained embedded development typically has (e.g. no dynamic allocation.) C is only as boring as you force it to be, and you really have to force it to be.
That said... the thing about the embedded space is that most of it is C, always will be, and may continue to be for the foreseeable future. It's really hard to really know what you have to regret if all you've ever known is C.
>If anyone tells you C is "boring", that's just plain and simple bullshit. C gives you undefined behavior, buggy compilers (yes even for simple C code, MSVC is especially bad at plain C but there are other offenders) and the world's worst standard library for manipulating strings.
Boring doesn't mean "has no issues" or "takes care of everything for you".
It means, stable syntax, lots of mature tooling and compilers, and any problems the language has are well known.
Of the various languages around, C is the least likely one to have "buggy compilers" (issues in MSVC because MS compilers focus on C++ are a different thing, also falling in the "known" category).
> It means, stable syntax, lots of mature tooling and compilers, and any problems the language has are well known.
In case of C what it really means is that the compiler codebases are extremely old and sometimes in rather bad shape.
Speaking of stable, C hasn't just stayed still: The C23 standard adds plenty of fun new things, like an annotation for unreachable code. Modern C has threads and synchronization primitives, #embed, complex numbers, and plenty more.
> Of the various languages around, C is the least likely one to have "buggy compilers"
C compilers are still routinely buggy, because the memory model is surprisingly tricky, there are many edge cases in the specification that are subtle and despite being fairly basic are not stressed often enough to come up, and because optimizing C while adhering to its rules about defined behavior is an unending arms race about what is still "technically" compliant with the standard.
Again, this is especially true if we're considering embedded development where the compiler you have might be a random hacked up old build of some vendor compiler rather than at least being recent GCC or Clang. In that case, even if you only consider a small subset of C99, there's still plenty of room for things to go wrong.
By any standard you can come up with, C is just plain-and-simple not a boring reliable solution. As it turns out, something being old and relatively simple isn't enough to make it boring; you can still have a pathological case that is just an absurd clusterfuck.
I will grant you one thing: it is the most boring embedded development solution. But come on. The best modern competition is Rust. Being more boring than Rust is not exactly an impressive accomplishment, especially when you consider how much practical value Rust has to offer.
No, because it's an example of someone who chose Go and didn't regret it, and continues to choose Go, because the objections of the article are in practice just not very important or compelling.
There's plenty of software I work on from time to time that's written in Go which I'm happy are written in Go. Some of those were even projects which I started and chose the language for.
Then there's software I've been involved in where I believe Go was the wrong choice. And software not written in Go where I believe Go would have been the wrong choice.
My point is, Go is just another programming language, with a unique set of strengths and weaknesses, and there are plenty of cases which i have experienced myself where there are genuinely no regrets around picking Go. Not because there aren't shortcomings in Go, but because there are plenty of cases where those shortcomings don't matter.
In my professional life, I have seen significantly more regret associated with choosing JavaScript on the back-end than with choosing Go. But then again, there are services in production written in JavaScript where that just never has been an issue and there are no regrets.
Where is the tradeoff analysis? Yeah, you might regret using Go when some zero value you forget to fill in somewhere pops up later and ruins your pipeline. But are you considering all the issues that didn't pop up because you chose Go?
Java's boilerplate code? Rust and C++'s lifetime analysis and memory management? C's lack of tooling? Python/Typescript's runtime errors? Functional languages' tiny employee pool? How much did trouble did you save by avoiding these?
Go is very boilerplate. It requires at least 3 lines of error checking every 1 line of actual code.
Also it doesn't have packed structs so it's completely incapable of doing low level networking or to handle binary files (you can of course do all the bitwise operations yourself… but is it sensible to use a language that is more error prone than C in 2024?).
Also due to its lack of A LOT of system calls, you will need to use modules written by someone on github, which will happily segfault if you look at them funny.
So now you have hidden memory management! Fun!
Of course if all you do with go is a glorified jq script… sure then it's kinda fine.
I’m not sure I understand the packed structs complaint. I have used Go to read binary data and it’s quite easy. You just need to ensure that all of your struct fields have fixed sizes e.g. int32 or [4]int64 or whatever. Unless I’ve misunderstood what you mean?
When last I tried it, maybe around 2014? I found it a kinder, cleaner Java with better tooling. Visual Studio (not Code) is still the best IDE I've ever used.
Unfortunately it's not popular in the circles I hang around in and the places I've worked. Now that .NET core is the one true runtime I'd welcome an opportunity to try it again; alas, I doubt I'll have such an opportunity (at least not through work).
I remember the upsides but I'm sure there are downsides I'm not aware of. I'd love to read a critique from someone with real world experience.
Not that you asked me but since Go is my goto language, my thought on C# is that it looks pretty cool. C# with hill-climbing thread pool and async seems rather compelling. I really see only two (major, obvious) downsides with C#:
- It has so much. language. design. This is both a strength and a weakness, of course, but C# really does take this to an extreme. I won't bother making a huge list of examples because I think you get the picture.
- Microsoft needs to stop doing things that harm confidence in .NET. Between the silliness of removing hot reloading from the CLI, the situation around debugging outside of Visual Studio products, and drama around the .NET organization... I feel cautious touching the .NET ecosystem. That's saying something considering the company that backs the Go programming language.
(Note: I have not used C# in production, so I can't speak to what it's like in that case. Seems like it's at least fairly "boring" in a good way for a lot of organizations though.)
Is there a specific aspect of language design that you see as problematic? I agree that it can be perceived as "way too many things to keep track of". I think most of it are small, atomic changes designed to reduce boilerplate that can be intuitively understood (like collection literals, additions to pattern matching and null-coalescing operators). You don't have to spend mental effort on these and if there is a scenario where more idiomatic syntax is available - the analyzer has a good chance of catching it and providing an autofix suggestion.
That is an interesting experience, as I find the opposite true in most of my cases.
And I am a go developer that likes go and use it for 95% of my coding, including MMO servers. I love programming languages and have used and dabbled in many. I still go to go.
And many go tools I use tend to be pretty well written. But maybe this is just a sample size of 1 vs a sample size of 1 issue.
From go programs at the very least I can expect they won't respect normal GNU getopt command line options, and work terribly with signals and going background in the terminal and so on.
If it's a server, of course it won't support any way of communicating it's ready. Not the old fork way and certainly not the new sdnotify things.
why would it have to respect GNU getopt? When did that become the golden standard? I never respect getopt because I really don't care about it and it has no bearing on anything I build. As long as they are documented under `--help`. Almost everyone uses Cobra from command-line options. And it is capable of doing getopt if you want, but I don't see why it would be a requirement.
Signals and Backgrounding seem to be just developers that have little experience with that stuff. I can't remember the last time I did any sort of signal handling to do anything specific. And I haven't had issues with backgrounding, but that might be just the tools I use and the infrequency of backgrounding that I do.
Most servers I interact with or work on have some sort of `health` api endpoint to get that status because most servers in go are HTTP. What more are you expecting? I don't even know what you are referring to by the `old fork way` but I will agree most don't use sdnotify, as that makes the assumption you are running Go on something that handles systemd notifications.
I am fairly certain a majority of Go servers run in a container, specifically an Alpine, distroless, or scratch container. So communication would be over stdout or some kind of API endpoint, be it HTTP, GRPC, or any other TCP/UDP endpoint.
I sort of like Go. The explicit error handling is a little obnoxious sometimes, but it's just a way to get things done. Otherwise I see its simplicity as a strength. It is very ergonomic, easy to pick up, and generally performs pretty well. I perhaps wouldn't pick it for every scenario, but there are plenty of scenarios where it would be a good tool.
Then again, I sort of like Java and Python too, two languages I am proficient enough at. All of those are good tools for what they intend to be.
I don't understand why people get so passionate about programming languages. They are tools. You may like soke more than others, but that doesn't invalidate the ones you don't like.
>I don't understand why people get so passionate about programming languages. They are tools.
Because when you're a professional programmer, tools are a huge part of what you do and how you do it, same like a race driver would need to be passionate about cars.
It's just that for an e.g. carpenter, tools are more or less standadized and simple enough to evaluate.
If saws and hammers and routers had as much variety as programming language tooling, and were as multi-faceted to evalute, carpenters would be absolutely obsessed with using the good ones - even more so than they already are.
> Because when you're a professional programmer, tools are a huge part of what you do and how you do it, same like a race driver would need to be passionate about cars.
I am a professional programmer. Have been one for more than two decades. And perhaps for professionalism, I think there is no space for passion when it comes to choosing the tools of the trade. Passion would make me pick unsuitable tools because well, I would be passionate. Passionate people don't tend to make rational decisions.
I would expect a professional carpenter to be the same. They may have preferences due to familiarity, positive experiences, etc and so forth. But passion?
I think if you tried to tell a professional carpenter that you'd replaced the contents of their toolbox with the equivalent pieces from a discount hardware store you'd be looking for your teeth on the floor.
I certainly wouldn't give up my electronic hardware repair tools without a struggle, it took me years to find ones that I like!
"Manager imposes it on you" just means you work in a team rather than alone. You can pick whatever you like for side projects, of course you're going to use whatever your team uses otherwise.
"The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt."
Excerpt of the talk "From Parallel to Concurrent" by Rob Pike from Lang.NEXT 2014
> People tend to forget that Golang was created on purpose for poor programmers.
Nobody is forgetting that quote. Trust me, it has been repeated a lot[1].
That said, I think this framing of the issue really needs to die. Rob Pike is saying they're "not researchers", that they're "typically fairly young", not that they're poor programmers. Notice that in the list of languages they may have learned, "C or C++" is present. The idea is not that Go is designed for people who can't possibly write C++.
This framing also implies that the language being better for n00bs means that it's also necessarily worse for everyone else. There are some tradeoffs where this is a defensible position, but I think on the whole it's just not generally true. A good example is preferring composition over inheritance: I think the former is generally more understandable and a lot of people actually contort C++ to use it this way too. (For example, in some codebases, only pure abstract base classes are ever inherited; everything else is final.)
When I see this quote repeated as if it implies that Go is just generally designed for bad programmers, I feel like it reads like flamebait. The real answer is that it was designed to be so easy that any idiot can use it. Or in other words, Go is very grug-brained[2].
To each their own, but it's been over 10 years since that quote and Go has evolved a lot. Is it perhaps time to put it to rest and stop reading into it so much?
> People tend to forget that Golang was created on purpose for poor programmers.
Poor programmers by Google's standards. I would argue the vast majority of programmers, even those outside of Google, don't want to be language researches, have no desire to be a language wonk, but want to build software to solve their and their companies problems. I read that quote and think it means that Golang is the only language the vast majority of programmers should be using unless they are researchers, not as a some veiled put down.
I managed to make go segfault multiple times (a real actual segfault). It's not a general purpose language. If you want to do things that aren't json RPC is awful.
If it was created on purpose for poor programmers, it seems to have been created to enable poor programmers to write the poor code they wanted to write, instead of making it impossible for poor programmers to write any code.
I guess that's the difference, if you want code, no matter the quality, you have one choice, if you want code that's correct, you have another.
I feel Go is simple in the way people say "C is simple" (without the footgun part)
It was created for one purpose and it kinda works but it's clunky. Like there are no fancy front loaders or backhoes and there's a limit on how much out of that form you can get.
You can call us poor programmers if that boosts your ego. The industry is still built upon people who can build fast. Go allows that and so does Javascript. You might not like it but that's what is earning most people their bread nowadays.
> The industry is still built upon people who can build fast.
Correction: the commercial software is, not the industry. The industry and corporations are capitalising on quality open-source software, meticulously written off working hours with a straight head and passion, and a great attention to details. The fact that you can write glue fast enough to satisfy your SLT is predicated on the presence of those quality components you're gluing together for free.
> which casts a doubt about developers actually liking it
I'm self-aware enough that I'm not clever enough for the likes of C++ or Rust; I wish more people were as aware of their own limitations. Managers have a long-term responsibility about a codebase too.
"Clever" developers have no place in professional software development, imo.
More generally, when it's not well suited for the problem to be solved. Eager coworkers anticipating Google level traffic may want to write the system in Go and multiple microservices when a simple FastAPI server would do.
I like Go, but after writing/reading so much Go code, I get nightmares from `if err != nil` demons punching me in the face. There were so many nice suggestions made to fix this, but there are some extremely conservative and extremely loud-spoken community members who killed all the proposals with vitriolic feedback. Now, the Go team has given up the battle for improving error handling since they are psychologically afraid of these folks.
Every Go developer survey has results where the community overwhelmingly votes to improve error handling, but a few extremists have derailed all progress.
> Go does not succeed in, because they are probably going to be complaining about it for the rest of their lives.
A lot of people really don't like Go because they have experienced other language features. Go has taken an arrogant stance at trying to make the decision about what features you might need and has given you a very small amount of things to work with.
Counterpoint, other languages - notably Javascript, Scala, PHP, maybe Java - have taken the stance that they adopt other languages' features, not because the language needs it, but because developers were clamoring for it. Which led to added complexity, because they kept adding more and more ways to solve a problem or structure an application, which led to every codebase being so different from the next that the amount of transferable skills and knowledge became less and less. Scala is the worst offender for that IMO.
One frequent praise one hears of Go is that a developer can open any codebase and immediately understand what's happening. Go is readable and stable, which are critical for code for the long term.
The one language feature that I miss in most languages is pattern matching. I wonder if there's any minimalistic language that implements pattern matching well?
I love the fact that I can pick up a Go project from 5+ years ago and it still compiles with the current toolchain. I might need to do a 'go mod init' first.
It didn't get 67 new features that start shooting deprecation warnings on old functions in the meantime. I don't have to learn new paradigms or switch integral parts of my code to something new.
Generics has been in Go for dunno how long, haven't used it once. Didn't need to.
I used generics once, was kinda useful, but definitely avoidable. The only feature I could see myself using is something Linq-esque for slices and maps. Otherwise I’m content.
And a lot of people using Go have experienced other language features as well and either decided against them or that the whole tradeoff was not worth it.
I will keep very fast compilation times and decade long backward compatibility over a lot of your features. Because those are features too.
I mean I miss some language features for sure, but the problem with adding language features is that it adds long-term inconsistency. Take a Go codebase from 10 years ago and it should look mostly the same as it would if it was rewritten in modern Go. Do the same with Java and across generations you'd go from straight for loops, to generic iterators and their for-each syntactic sugar, to for-comprehensions, streams and their functional programming style, to whatever Java is up to in 2024, I stopped paying attention years ago.
Then you're doing yourself a disfavor by using Go. In other languages it would be even more explicit, mandatory, and automatically checked whether it's handled!
Exactly. Every programming language is a tool in your toolbox, and you should choose the appropriate one for the job at hand. For me, that's Go around 95% of the time.
I have no need to worry about a 24 byte overhead for a slice allocation, if I did have to worry about that, I'd probably use C or Rust.
And since Go is so readable, theoretically getting the core functionality out and rewriting it in a more specialized language would be fairly straightforward. And while it's an investment in time and effort to rewrite a chunk, at least you know what you're writing already.
But that's a point made in the article, that Go is also good for prototyping. But there's a few languages good for that, e.g. Ruby which powered a lot of the HN startups in their early days until parts needed to be rewritten for performance / scalability.
But writing a new, unproven product in a high performance, high difficulty language from the get-go is cargo cult; you hope you will need the performance and correctness offered by the language. Meanwhile, halfbaked hack jobs like Facebook and Twitter exploded in popularity and revenue, and while their performance issues gave their developers headaches, at least they knew what problems they had and had to solve instead of guessing and hoping.
> And since Go is so readable, theoretically getting the core functionality out and rewriting it in a more specialized language would be fairly straightforward.
You'll have to rewrite your whole program (or at least factor out the "core" part you care about into a separate binary and talk to it via IPC or network) because Golang has terrible FFI as pointed out by OP.
Every time I work in a different language I'm always wanting to go back to Go even if it's not the perfect language.
I just love the fact that it literally just works. You install Go, you download code, and write code that's it.
No figuring out what version, runtimes, configurations, build tools, package managers to use. Just install and Go.
I think maybe Rust is the only other programming language that provides the same experience.
Maybe these are just lies I'm telling myself, but every time I use Python, Typescript, or Java I dread programming because I just want to write code and I'm often debugging things or figuring out related to configurations, package managers, build tools and versioning.
This is the power of Go, its integrated toolchain. It makes interop more difficult like the article says, but in my personal and limited experience that's not a frequent use case.
> I've started caring about semantics a lot more than syntax, which is why I also haven't looked at Zig, Nim, Odin, etc: I am no longer interested in "a better C".
Well the post rambles a fair bit, IMHO. The whole bit about Go being “accidental” is BS given that Rust is just as much “accidental” in its origin and design.
One thing stuck out to me is that Nim certainly isn’t a “better C”. It has a GC or you can use reference counting. You can use it as a better C if you really want.
Nim’s type system avoids many of the problems that Go has, though it’s not nearly as pedantic as Rust.
At the end of the day lots of software has been written and shipped in Go which runs fast, has minimal downtime, generally seems effective, and has minimal security issues. I’d argue (much) fewer software projects have been shipped in Rust. Firefox is still 95%+ C++.
> I’d argue (much) fewer software projects have been shipped in Rust. Firefox is still 95%+ C++.
It's funny but this comment reminded me of this tweet[0] from 2022 (!). I don't have a horse in this race as I am happily using Python and C++ at $DAYJOB. I'd argue that even if much less software has been written in Rust (source?), it still qualifies as "lots of software has been written and shipped" with it. Not to mention all the investments by $BIGCORPS in the language.
I think this is exactly the right way to understand Go - it's targetted at building servers in environments where having strong consistency of code and a short ramp up time for junior engineers is valuable - i.e. it's perfect for all the big corp scenarios that Java was used for.
I think maybe the more common, but less helpful comparison of go vs rust comes from the fact that they are both part of a new wave of languages and that they both default to producing staticly linked binaries.
There are many stylecheck tools that should be apart of a good stack. Accepting the creator's style is putting a lot of weight on their opinion. Most organizations have their own preferences for good reason.
> short ramp up for junior engineers
Junior engineers aren't a place you're concerned on being productive. Most of the time at that stage in someone's career they should be learning more, getting up to speed with professional practices, tools, and trying to learn code bases+patterns. Ramp up time for a language is a very minor consideration.
Both of those things have very little to do with server environments.
Bigger corporations struggle with Go's module system and forced reliance on seperate repos for separate modules. (Can you bundle multiple modules in the same repo.. yes but you're going to have a bad time)
> Both of those things have very little to do with server environments
My experience of bigcorp is that they need lots of servers (http is the modern bailer twine) and want developers to act as far as possible as indistinguishable resource. They will have rotating, high churn, globally distributed teams of vendors, contractors, consultants, internal staff and the teams will encompass a vast range of skill levels and abilities.
Some languages amplify skill level disparity, some attenuate it.
It's a really bad choice for situations where Java is a good choice as not only is the language limited, the ecosystem around it is also very limited when compared to say Java.
I'm maintaining Go, C# and TypeScript as my main languages as that gives me excellent coverage. I'll add Rust to the mix when I have 6 months where I can accept the productivity drop or have a project where the quality requirements are lower (it only takes a week or two to pick up a language, it's learning how to engineer within the ecosystem which takes the time).
How is Go an iteration of C? You can't use Go to write a kernel, or program a microcontroller, or for high-frequency trading or a web browser or a tensor library or a language run-time. It's either a bad idea or simply impossible, depending.
Someone please explain to me what's C-like about Go other than vaguely the syntax and that it compiles to machine code.
Not a production kernel, but MIT did use Go to "study the performance trade-offs of using a high-level language with garbage collection to implement a kernel" [1]
There is also gVisor [2] which implements, as best as I can describe, a kernel in user space. It's intent is to intercept syscalls made in containers and to redirect its execution in a sandbox.
> ... program a microcontroller ...
I'm not sure if one would classify this as a microcontroller, but USB Armory did write a, iirc, Go compliant runtime for bare metal ARM and RISC-V [3].
There is also TinyGo [4] with the following listed microcontroller support [5]
The Go language was actually created while waiting for C++ to compile. Their goal was to create something that was better than C++ for network services and they succeeded- but that's a pretty low bar! Most companies that don't have major performance concerns don't use C++ for networked services. If Rust was already mature it would have been a good option- except for the compile times which was one of their main original issues could well be worse in a Rust code base! I have seen this myself- Go being used instead of Rust with the biggest reason being it compiles so much more quickly.
Because someone decides what language to write a new thing in is very likely to consider Go and Rust. They are very unlikely to consider Java.
Are Rust and Go sufficiently different that they should each be chosen in different cases? Sure! But that’s literally why someone would consider both and pick one.
People have an irrational hate for it based on the enterprise cruft and horrible third party frameworks you can just completely ignore if you build a new thing
It's not good for commandline stuff but for a long running small service it is pretty great
That's a big if. In practice you're not going to be able to escape needing to interface with Java code others have written. What then? You either waste time putting up shims in front of every single API or, more likely, give up and just go with it, at which point you've lost.
It is much more practical to choose a language that does not have a terrible history of poor API design to begin with.
^ this! Java's far more capable than golang and a better choice for many projects.. just don't use the decades old "Enterprise" stuff.
..and I'm generally a Java-hater as the language itself is objectively inferior to my beloved C#.. but even then there are situations where Java is a better choice.
While the article raises some valid critiques, it often overlooks the fundamental tradeoffs that make Go successful in its niche. Go’s simplicity is not a flaw but a deliberate choice, enabling rapid onboarding and maintainable code for large teams. Its explicit error handling may seem verbose but ensures clarity and avoids hidden surprises common in exception-heavy languages. The complaints about ecosystem isolation and tooling ignore the fact that Go provides powerful built-in tools like pprof and dlv, which are better suited for Go’s runtime than general-purpose alternatives. Go isn’t trying to be Rust or Python—it’s a pragmatic tool for scalable, performant systems.
That should not be an issue, unless the code is written in notepad.
As for interfaces, a better approach is to return struct and accept interface in your functions wherever it is possible.
I love Go, so I am biased. However, the beautiful thing about Go is that it doesn't even attempt to prevent classes of bugs by making them impossible. It's a car with ABS but no lane assist, with power steering but no collision detection.
Out of all the bugs which Go permits I have yet to see one which could survive in production for some time without being discovered. Almost all of them would cause a failure the first time a segment of code is run.
I’m successfully using Go and agree with the author at the same time. For me it’s not a choice between Go and Rust- Rust is not as productive for high level code because every API maximizes performance.
Since this article was written Go introduced generics which does solve some of the complaints. The rest I mostly solve with linters, libraries, and conventions.
Go has a syntax that is well designed (arguably one of the best) if you’re aiming for familiarity with C style syntax in an imperative language. Other than that it’s a poorly designed language with by far the best engineering work having gone into things that aren’t actually the language per say (cross compilation, asynchronous runtime, GC, module system, super fast builds, etc). They wrote an ACM Queue article essentially stating that: https://cacm.acm.org/research/the-go-programming-language-an...
It's weird. At a time I was looking for a "better" python. Something simpler and safer than C/C++ but faster than python and more importantly that can produce a single binary. So I looked at everything from Rust to obscure language like Hare.
Go should have been the obvious choice but for a reason I don't understand I dislike its syntax. For Rust I understand: it uses a lot of special characters that aren't easy to remember and to type with a non qwerty keyboards (plus other unrelated pain points). For the different lisp, it was the parenthesis and the reverse polish notation. But for Go, I'm unable to rationalize why I don't like it.
For the anecdote, I settled on compiling my python code with nuitka. No speed gain that I'm aware of but I can now provides a binary. I'm also looking more and more at C# because of its progressing AOT compilation and although I dislike the verbosity of its base mode and the fact it's so tied to windows.
I liked a lot nim and crystal but the small community was a barrier, although I'm really impressed by what nim is managing to do with such a small community and it may me think it's an excellent language
(I will try to motivate myself to pick up one of the language I mentionned above also)
Yup, common misconception for folks that haven't used it in a decade.
Our team uses C#. We dev on Apple silicon Macs. Some use Rider, others just use VS Code. We build on Linux via GitHub Actions. Ship to prod running AWS t4g Arm64 instances.
C# to me is like TypeScript++. The language, syntax, and core constructs are close enough that anyone with a good handle on JS and TS can pick it up easily and be productive.
Sorry for the segue, but how your team's experience with C# on VSCode? Any recommendations for plugins? I've heard of a lot of people recommend Rider but not much, aside from neonsunset, talk about VSCode.
Just in case: DevKit is optional and requires an account, you can just use the base C# extension which is what provides the language server and the debugger, if you prefer VSCodium there's a fork of it which packs Samsung-authored netcoredbg instead of vsdbg so that is covered too.
For F# - Ionide works great, I like it a lot, integrates seamlessly with existing C# projects.
Go is great for a variety of projects. Smaller codebases, little cli tools, simple servers, etc. It has its quirks, as any language does, but it is foolish to write off an entire language when some of the very properties you dislike may actually be a good fit for the problem at hand.
When engineers focus on language features they focus on the wrong things. Yes, languages like Java and Rust are fantastic for large codebases because generics and strong encapsulation become especially important as the number of engineers working on a project scales. Contrarily, Go would probably be a million times better for a small project of one to three staff that primarily revolves around a few well-designed data structures and basic array manipulation. In this context, the more sophisticated features of a Rust or Java are actually hindrances and end up making things more elaborate than needed.
Languages are a resource and a tool, not an identity marker. Use the language that makes sense for your constraints. Programming is all about modeling problems. No language will do this for you and each one has features that help or hinder and some are better or worse form problems of different shapes. Learn about how computers and compilers actually work so that you are empowered to use a wealth of languages and can choose the best one for your project on rational grounds.
Or, you can embrace Haskell and use the only perfect programming language to have been thus far invented :)
Go is just an evolution of C, it works at a higher level but shares many ideas. If you can’t appreciate the simplicity of C, then you probably won’t appreciate the works of Bell Labs, and I have lost all interest in the debate.
I have recently started to port a large codebase from Go to Rust. I thought it was going to take more code in Rust, but I have been surprised at how much less code it takes. As of right now, the Rust version is 60% smaller in terms of LOC.
I think a large part of this is due to error handling in Rust and the sugary `?` syntax which is so incredibly useful.. I can't count how many times I have done the following in go
x, err := Foo()
if err != nil { return nil, err } // or something like this
I'm pretty sure a large chunk of the LOC reduction was in that alone. Though, a large chunk is also some crates that make a lot of boilerplate go away (serde, serde_json, etc).
The argument about FFI and CGO was the most unappealing to me. If you really need to do that, it feels that at some point some decisions were made that weren't the right fit for the project.
Also, I feel that many arguments in the article are potential pitfalls for unintended side effects I've experimented in other languages, like Ruby itself. Keyword arguments are nice, but I know that they've bitten me several times.
Go forces the developer to be explicit about their intentions, that's why I also don't find something bad that your struct functions need to have the pointer receiver. It can be annoying when you forget, but that's what linters are for.
CTRL+F "rust", 24 matches, I had a feeling that would be the case. What does Golang, for the most part, have to do with Rust? I also find the following bit somewhat funny:
>The success of Rust is due in large part to it being easy to adopt piecemeal and playing nice with others.
And as if Rust itself didn't suffer from the same kind of imperfections one can find in Golang. So much for that "nothing new under the sun" back in 2012! But then it starts talking about the "rust shills" boogeyman, and one has to wonder if it's not trying to justify one's choices. (which is fine, anyway) And I agree wholeheartedly to each and every single one of the "lies" listed in the article, that you could very easily rewrite half of which to fit Rust, and the other half requires no changes to apply.
- "Go is an island": Fully integrated toolstack makes it easy to use the same tool top-to-bottom, but makes it difficult to use with any other project
None of those are lies; apart from the first, they all have advantages and disadvantages. Sure, there's an advantage to forcing you to specify every value, but there are disadvantages too. And sure, there are disadvantages from Go being an "island", but he lists some of the advantages right there in his post.
If those are deal-breakers for him, then sure, go use something else. But lots of us like the advantages more than we dislike the disadvantages.
I liked the idea of a language with minimal syntax that is easy to learn and easy to understand because the code is forced to be straightforward. It didn't work out that way to me in practice.
It's a bit too low level for many use cases in my opinion, and that does get in the way somewhat. It also works against the "easy to learn" part unless you start with developers familiar with low level programming.
I also found some types of library code surprisingly difficult to read, especially when empty interfaces where used.
The standard library was great though, it covered stuff that others don't and it just worked out of the box.
Currently onboarding to go. For me personally, it's too opinionated e.g. for: error handling, mutability, unit testing, naming conventions, lack of features like sum types.
As a new joiner, some things have a "religious" feeling, like: the go gods have given us THE language and we must use it their way, without questioning too much.
I have the feeling other languages are less opinionated and this allows for more ergonomics and comfort when developing.
It's a language built to make it hard for new joiners to mindlessly add complexity, as the language itself will fight you. It makes it hard to add dependencies that aren't very visible and vendored in, it makes it hard to change how the language behaves with overloading, preprocessor magic or macros, and so on.
It's built to fit Google specifically, with their army of new grads, and deep investment into reinventing all the dependencies themselves, and having a huge monorepo.
If you're not google, you're gonna have a bad time, which is also good for google.
The article is arguing that Rust is a better choice which is laughable. Go is a small GC-ed language. Rust is an obnoxiously complex language with strong emphasis on memory management. I don’t see any projects where you could legitimately hesitate between the two.
Truth be told I quite like Go. It’s small and simple. It does the job. You can onboard people on it quickly and in practice with the toolchain you don’t run into the issue mentioned.
Would I rather use Ocaml most of time? Of course but it doesn’t have the same ecosystem. Would I rather use Rust? Heck no, I’m not going to deal with the borrow checker and life cycles if a GC is fine.
>After spending years doing those FFI dances in both directions, I've reached the conclusion that the only good boundary with Go is a network boundary.
It works perfectly well with stdin/stdout as well, as seen in many LSPs
> I've started caring about semantics a lot more than syntax, which is why I also haven't looked at Zig, Nim, Odin, etc: I am no longer interested in "a better C".
A strange take. Zig, Nim and Odin are about fixing semantics not to bring syntax sugar.
> Because they're Go experts, they know the cost of using Go upfront, and they're equipped to make the decision whether or not it's worth it
Well maybe not.
If I’m an expert in only Java 6, I might not be aware of all sorts of useful features that other languages have, such as sum types, traits, type inference… I only know one side of the trade off.
I might be vaguely aware of those ideas and dismiss them because I can’t imagine how they would fit in my Java 6 workflow.
And yet when some of them arrive in later Javas, I begrudgingly use them. Years later I can’t imagine how I ever lived without them!
We’ve seen this play out so many times.
Remember when lambdas were just for weird functional languages?
Seems like Zig is actually meeting much of the writer's expectations for good language design. Although I'm still not wholly convinced by it's move from LLVM and hell-bent desire for incremental compilation.
I see this false promise all the time: Simple tools are simple to use.
NO. Not necessarily. Use a handsaw to fell a forest if you like, or a hoe to till 1,000 acres. But I will not. Tool simplicity will come back to bite you if the task you're trying to accomplish is complex or high-scale. And simplicity of design in no way correlates to simplicity of outcome, or of your workflow using the tool.
Golang has, to be fair, more going for it than just its relative simplicity. But in many ways the OG criticism rings solidly true. The "we'll do just enough language features and tooling" becomes an abdication and an excuse in a complex world.
I want to build things and could care less about intellectual masturbation. It was built for proletarians like myself. I like Go because it's neither Rust nor Python. But I do understand that there are times when having some nice abstractions over common patterns doesn't hurt. At the same time, Go has come a long way over the years.
This again? It's reassuring to see the moderation on HN hasn't improved one bit, still as selectively enforced as ever and blind to even the most thinly veiled trolling.
Since apparently this needs explaining, the post title implies everyone who uses Go is delusional, unable to think for themselves, an idiot. The article of course follows suit. How such a strongly emotionally charged article is supposed to lead to intellectually curious conversation [1] eludes me.
Dang's comment in the original 2022 thread [2] is downright hilarious to read. "These flamewars around emotionally charged articles keep happening and we're powerless to stop them!"
Enforce the rules around flamebait fairly, regardless of who the author is and you won't have a problem.
"Accident" is hyperbole, but IIRC Go evolved out of Google's internal high-performance HTTP server tooling, so they didn't initially set out to create a general-purpose programming language.
My biggest problem with Go is readability of some projects. In many cases, the code is split across files in an unintuitive way, and you can only navigate with the help of an IDE.
One thing what i observed is any language you right ask AI to optimize in Rust..why its so much of a debate when scale comes. In 6months AI will make any rewrite possible with full context aware. So don't spend your energy on things that can be done by machines and focus on things which u can get deep on for distributing the software.
Well, the post title implies people using Go are delusional, so that's expected. Starting a conversation by calling someone stupid isn't likely to end up productive.
There's a lot of valid critique of Go, but I've never found anything like it that lets me build lasting, high quality, bug free software.
Explicit error handling means that I actually think about and handle errors. No surprises in production. No random exceptions throwing deep inside some dependency. I've been running some Go services for years with no crashes.
The brain-dead simplicity means I am not tempted to waste time being clever.
The tooling means my code is trivial to build, even years later on a new machine.
I wish I could take the Rust pill like everyone else and be happy.
I am not a stranger to the programming world. I am fluent in Python, Java, Scala, Erlang. I can write acceptable Haskell and C++ and Common Lisp. I am well versed in functional programming (though I don’t hate OOP — it is still a good thing when done correctly, and I am a big fan of Smalltalk).
However, for some reason, I am completely unproductive in Rust. It always fights me. There’s always something I want to do (graph structures? spliced lists?), and it’s always “you don’t need that” and “it’s not the Rust way”. There’s always explicit lifetimes and you miss something and you need to rewrite huge swaths of your code. And nothing is refactorable. It’s not even a good functional language, there are no persistent data structures, and it’s a pain to make them, and no ways to express yourself functionally like I used to do with Scala.
But apparently, everyone and their dog like Rust and are insanely productive in it. What am I missing?
I do miss several of the things you list, too, especially (safe) self-referential data structures. But Rust is a fantastic compromise solution if your number one priority is to match the performance (or rather, performance possibilities) of C and C++, and have memory safety while doing it. If your number one goal is to be able to write memory safe ideomatic code that is entirely comparable to those languages, and then add some functional programming and lots of quality of life stuff to that, then it's pretty sweet.
In other words: Don't think of it as rivalling the entry cost of Python or Java, or the expressiveness of Haskell or Scala. Try to think of it as a replacement for C++, with memory safety and piles of quality of life improvements. And then with a sprinkle of the other things on top.
I am fine with C++ memory safety if I am mostly working with RAII and explicit move semantics when needed (it is not perfect, but it will get us through the night). However, fighting the borrow checker drastically _decreases_ my quality of life, and I couldn’t find the way to get myself to free flowing code as I had in other languages.
Interestingly, I have the same feeling when trying to write something with Vulkan — the amount of ceremony and boilerplate before you even get to the first triangle is a huge barrier for my productivity. I want something more immediate.
I just want to say that I started working with Go a few months ago at my new work, and I've been disappointed. The type system doesn't even have union types, and the implicit implementation of interfaces has caused a couple headaches that I would've never had with explicit languages such as Typescript (strict).
At this point, I prefer strict Typescript with very restrictive ESLint rules than Go. I don't understand how this language was pushed for web servers, it lacks common constructions to represent very frequent data types in API design.
Also, the whole go func(){} and channels is nice if you are coming from a language with worse ergonomics (C?), but when doing multiple web requests to other places and wanting them to happen in parallel, it gets messy fast. It lacks a pretty abstraction -- I very much miss an await-like construction which uses wait groups under the hood.
So, I very much agree with the post. I like errors as values, though.
The superpower of go is goroutines and channels. The kryptonite of go is its limited libraries. Go is a great choice for many concurrent applications. I couldn’t finish reading the article because it lacked focus.
There _are_ two problems with Golang that I _would_ like to wave a magic wand and fix if that was a power I had.
1) Sum types (E.G. C/C++ union types) - Yeah, something similar to that should exist... it's syntax sugar over #2
2) 'casting' / reshaping perspective on data ; as long as something has the same struct size a programmer should be able to tell the compiler 'but this is actually that'. Which also implies a way of fixing the layout of a given data structure. I figure that's why Golang doesn't allow this already.
Yeah, 24 bytes (len, cap, pointer) per slice (dynamic array) has a cost, but if that really gets your goat use a fixed size array, or pointer/reference to such [n]thing.
3) Seriously, for a slice, make it like a map, a pointer/reference to a len,cap,blob - so that when I pass a slice by value I pass the WHOLE slice, not a COPY of len and cap and a reference to the slab. Current golang has the worst of all worlds with passing slices around, in that changes mutate, UNTIL it's resized or manually copied. The current design has the behavior it does to support slices of slices, which requires the pointer to the blob. A more complex scheme of a container behind the slice, and references to that could also work, but would be an entirely different datatype.
Well, i feel like this is the 100th article i read about why golang is bad "mkay".
For my personal background, i started with golang about 6 years ago and im using it mainly for private and open source projects.
Yes golang for sure isn't perfect, but what language is tho? I think the major point is - the language you use should match your use case. If it doesn't it will always feel "bad" or you will more likely tend to find points why the language isn't perfect (for your needs).
Sure you could write a website builder in ASM or you can write an Operation System in Javascript - but why should you?
Just look at your use case - check your needs and decide what language fits the best.
If its golang? Fine use it. If its not golang, than don't. But don't take a language and try to match it on "everything" and than complain that it doesn't do the job - because no language will.
Even async in Go isn't that good, ultimately. You can't monitor channels, you can't properly react to errors, you can't shutdown and restart them. A panic in a channel will kill your program. Etc.
It's "we learned about green threads and didn't bother to learn anything else" approach (also prevalent in many other languages)
> I've mentioned "leaving struct fields uninitialized". This happens easily when you make a code change from something like this:
Really? Are you not using gopls? It absolutely will warn you about this. And the mutex case. And a lot of the other similar criticisms.
> Go not letting you do operator overloading, harkening back to the Java days where a == b isn't the same as a.equals(b)
In languages that have it I've never used operator overloading to produce anything good or that was obviously better than just using methods.
> The Go toolchain does not use the assembly language everyone else knows about.
Honestly I've never noticed or had to care. And you can build plenty of things that don't require CGO.
The whole "gnostic" tone in language articles on HN is becoming overbearing. There are parts of the language to criticize but couching it in this "lies _we_ tell ourselves" and somewhat kitchy writing that presents minor matters of taste as fully existential issues makes this an unsatisfying read.
> In languages that have it I've never used operator overloading to produce anything good or that was obviously better than just using methods.
I used a Scala library for S3 once that overloaded + to mean upload.
var bucket; var file;
bucket + file;
Which is obviously bad and unnecessary.
It's really a feature that should be used very rarely by those who make basic libraries, but there it can make a lot of sense - data structures, numeric types, etc.
The article is great. The bigger picture, of course, is that it’s always “pick your poison”.
On one hand, say, I love operator overloading, I love how Python does it (once you satisfy an interface, its operators Just Work).
On the other hand, I can appreciate the choice not to do it at all because half of the ecosystem will do it, and another half won’t. Also, it would require implementing function overloading, and it is a can of worms.
Or generics and rich type systems, which all come with their own tradeoffs. I hear that Rust cajoles you into tinkering with the type system, and wach tweak requires refactoring of more codebase than anyone would like (don’t take my word for it, it’s just what I heard from a few different sources). I know that Nim is so expressive that it can be annoyingly trivial to be too clever and run into a valid edge case that will make the compiler barf and die. Go sidesteps the issue by not wading into that territory, and that may be perfectly okay, albeit verbose.
It’s always picking your poison, so I guess check your tolerances and allergies so it doesn’t kill you before you get the job done…
> I love operator overloading, I love how Python does it (once you satisfy an interface, its operators Just Work).
Is this different from other styles of operator overloading? Why does it matter whether, when I want to overload the + sign, I need to define a function called `__add__` or `operator+`?
This is a silly article, though I can only speak for myself. First of all, language design is pretty much the last reason anyone should consider when choosing a language. For me as a solo developer who needs to get results in time, the main criteria for choosing Go were compilation speed, development speed, general tooling, and third-party support. Especially the latter is extremely important; I cannot develop all kinds of libraries like e.g. an Excel or Word document parser in-house, I have to use whatever MIT-licensed libraries that are there.
I've developed in all kinds of languages in the past: Pearl, Java, C++, REALBasic, PowerMOPS (a Forth derivate in the 90s), Ada, Rust, Python, Java, Racket and various scheme dialects, CommonLisp, ObjectPascal/Lazarus, just to name a few. Out of these, REALBasic was by far the one in which I was most productive. Alas, it became unaffordable and the vendor lock-in really burned me. No more commercial languages or IDEs it is for me.
If Ada had a garbage collector, I would probably use it even though the development time is a bit longer. Likewise, I'd love to use CommonLisp and think it's the best dynamic language. But it simply doesn't have enough 3rd-party support, and I'd also be wary about how it runs on mobile platforms.
I've got to say I'm pretty happy with Go so far. Is it the ideal language for me? No, because full OOP with multiple inheritance would make my life much easier. But it works, is easy and fast to develop in, and the results speak for themselves. I have no problems with bugs. The explicit error handling may be a bit cumbersome but it forces you to think about and deal with every error, unlike people in languages with exception system who often ignore errors until so far up the call chain that they don't even know what actually happened any longer. I don't see the point of that. If you have an illegal state of an object it doesn't matter if you call the object nil or DefunctInstanceOfBla, you're going to have to deal explicitly with the illegal state.
Notably, C# was also in my final selection of languages. For my principal use case - distributed client/server applications with GUI - Go's GUI options were not so stellar and I was thinking about using C# instead. AFAIK, C# is very suitable and a great language, too. I decided against it because of the C#'s bizarrely complex runtime library situation and naming schemes (WTF?) and simply because I would have had to learn it first and already knew Go fairly well.
Beware the language aficionados and purists. I used to be one of them, too, advocating Scheme & CL. However, in the end purely practical considerations are always more important. Programming languages are tools for getting things done.
The author's last post they referenced is a bit bizarre. I don't think that some overly simplified and error prone std library APIs is a particularly compelling reason to dislike a language. I didn't read the entire thing though because it was extremely long
“An idiot admires complexity, a genius admires simplicity, a physicist tries to make it simple, for an idiot anything the more complicated it is the more he will admire it, if you make something so clusterfucked he can't understand it he's gonna think you're a god cause you made it so complicated nobody can understand it. That's how they write journals in Academics, they try to make it so complicated people think you're a genius” ― Terry Davis
Please note, no offense intended, I just like this quote to describe Go success.
Context for those unaware: This article was a response to a 2022 HN thread [1] about the original article of the author's. Then the following discussion happened: [2].
First off, let's appreciate the fact the author has managed to write this humongous article in a day. That's some writing skill.
And indeed, leveraging writing skills to unleash a flood of words in order to overwhelm anyone trying to respond is just the first tactic of many used by the author to sound more convincing, which is going to be the point of my post. I'm going to analyze each rhetorical device, fallacy, and bias present in the article.
> The author is a platypus
The point of this paragraph is not to present any arguments, it is devoid of them, but to elicit sympathy from the reader for the poor author who is clearly getting ganged up upon. Comments in the referenced HN thread did not in fact attack the credentials of the author in any way. See Appeal to pity [3].
> Mom smokes, so it's probably okay
Lots of words here, but the gist of it is:
> Or you can be horrified, as you realize that those complex problems only exist because Go is being used. Those complex problems would not exist in other languages, not even in C, which I can definitely not be accused of shilling for (and would not recommend as a Go replacement).
Setting aside the validity of the technical arguments, the tactic used here is to imply that since certain other languages don't have the specific mentioned problems, they are strictly superior. In reality, had another language been used, the Tailscale team would simply be dealing with different problems. To riff off the C example, if Tailscale used C, they'd be dealing with memory safety issues instead.
> The good parts
A clever attempt at a lie of omission. First we list a tiny subset of the pros of Go, then we claim that this tiny subset is not enough to make up for the numerous cons. See Cherry picking [4] and Straw man [5].
> And since, just like C and Java, you do not get to decide what is mutable and what is immutable (the const keyword in C is essentially advisory, kinda), passing a reference to something (to avoid a costly copy, for example) is fraught with risk, like it getting mutated from under you, or it being held somewhere forever, preventing it from being freed (a lesser, but very real, problem).
Here we see a thinly veiled reference to the main bias behind the article. Can you guess what language the author is implicitly talking about here?
> Go is an island
Nothing really interesting here, but it's worth noting that, just like the majority of arguments presented so far, this largely applies to most languages out there (but of course not to the language we're really talking about here).
> All or nothing (so let's do nothing)
The thrust is another strawman, quite simply nobody argues this: "It's not like you can prevent all problems anyway.".
This section is only a lead-up to the next one anyway...
> "Rust is perfect and you're all idiots"
Huh, why do we suddenly have a tirade about Rust in what's supposedly an article on Go? If you correctly guessed earlier that this entire article is actually about Rust, congratulations.
> Folks who develop an allergic reaction to "big balls of mutable state without sum types" tend to gravitate towards languages that gives them control over mutability, lifetimes, and lets them build abstractions. That those languages happen to often be Go and Rust is immaterial.
Wait a second, language*s* with sum types, control over mutability, and lifetimes? So, Rust, Rust, and Rust?
> Because function signatures don't tell you much of anything (does this mutate data? does it hold onto it? [...]
Only Rust is good enough, we get it already.
---
Overall, we have quite a few strong technical points here that could have made for a compelling article, but they're marred by copious use of fallacious arguments and blind fanboyism for a certain language.
The author clearly does not weigh in on the "whys" some things work in Go like they do.
So, adding an extra struct field results in the base value, and by design this should be used as a base default. The go proverb make base values useful goes hand in hand.
Obviously functions are not the same. And calling function with the wrong args is a compile time error. There is also CI check that check for this use case.
Overall this seems like a comparison to Rust, and its obvious they are not same same. Apples to Oranges.
Rust is also not the same as C, and therefore shall not be compared to it. Crabs to Cats.
Of course they are not same; that's why we want to compare them in the first place: were they the same, there'd be no need to compare them at all. Since, you know, they would be same and have no differences.
I really wanted to like go, and I tried to write a discord bot using it, but the very opinionated brace style (which isn't the one I prefer to use), and the fact that I struggled to much to try and split my code across two files kinda turned me off it. In the end I just went back to python
AI can rewrite any dream code which u write and putting ur brain on that is pure seless way of pointing start with some island as you pointed get to scale and in 6months every code can be rewritten how many ever time u want..tokens will be so affordable to run full context code base shift..don't worry about it just focus on distribution for ur product with the code you right..thats a good thing to worry about.
This article makes a lot of great points about the shortcomings of Go. I don’t think explicit error handling is one of them however. I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up. From a technical standpoint that isn’t necessarily a good argument, but from a pragmatic standpoint and decades of experience… well I will take explicit error handling which happens exactly where the errors occur every day. You can argue that Rust does it in a more elegant way, and I prefer it for personal projects. For big projects with a lot of developers of various skill level joining and leaving I think Go’s philosophy is one of the sanest approaches to error handling in the modern world.
Staying in that lane. In my part of the world Go is seeing adoption that no other “new” language has exactly because of its simplicity. It’s not the best language, but it’s often the best general purpose language because it has a lot of build in opinions which protect you from yourself.
There are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum type.
No stacktraces and error wrapping forces you to not only invent unique error messages. You must also conceive a unique wrapping message at every call-site so that you can grep the error message and approximate a stacktrace.
The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language, and the awkward variable initialization rules, make it so that you use the wrong `err` var at some point. E.g. if you want to reassign the result to an existing var, suddenly you have to declare `var err error`, and if `err` already exists then you have to reuse it.
There should be an enum type in go, or instead of the bizarre "return tuple" mechanics exclusive for errors, they should have added a better syntax sugar for errors like rust's `?` sugar. Instead we have something extremely tedious and quite error prone.
> it has a lot of build in opinions which protect you from yourself
It does have opinions, but too often they seem to be there to protect the language from being criticized. Sadly, this works, as marketing (lying) is an important factor towards making a PL popular in today's market.
I forgot:
The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface.
IMO it's okay to make behaviour-exceptions specifically for error handling. Rust for example doesn't really have builtin behaviour exceptions specifically for errors, they're generic to sumtypes and just happen to work well for errors. But then in practice you must resort to thiserror or anyhow helper crates to deal with errors in anything but tiny programs.
If you do make behaviour exceptions for error handling, be honest and upfront about it. Don't say "errors are just values so just use them like regular vars" in docs, if then there are several huge exceptions (tuple-returns and breaking a core tenet). If you make exceptions then you might as well do them right, instead of half-assing them. I believe zig does it right, but I haven't gotten around to try it.
"Do what I say, not what I do" is almost a design guideline of Go at this point, there are so many inconsistencies like this in the language design.
> The tenet "accept interfaces, return structs" is violated all over by returning the `error` interface.
To be fair, that expression came from a blogger who often wrote about Go. It is not a tenet held by the Go project. In fact, Rob Pike has made clear that he considers it to be misguided advice. It is only violated in the same way returning Result in Rust violates the assertion I made up for this comment: Do not return Result.
https://github.com/pkg/errors provides stack traces support. This unfortunately did not get included in the Go1.13 release when error wrapping was introduced.
> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language
MRV, go does not have tuples.
Go is not the only language with MRV (as a special case) and they’re not necessarily bad, iirc Common Lisp uses them as auxiliary data channels and has a whole host of functions to manipulate and refit them.
Go is singularly incompetent at MRVs though, in the sense that only syntax and builtin functions get access to variable arity (e.g. if you access a map entry you can either get one return which is a value, or two which are the value and whether the key is/was in the map). So MRVs mostly end up being worse tuples infecting everything (e.g. iterators needing Iter and Iter2 because you can’t just yield tuples to for loops).
> MRV, go does not have tuples.
> MRVs mostly end up being worse tuples
I think you noticed yourself that you’re getting too hung up on terminology. Multiple return values are a half-hearted, non-reified version of tuples.
No, MRVs can actually offer useful properties and features, that is what they do in Common Lisp. That Go does not do that has nothing to do with MRVs.
Which is what they said. I'm not sure what point you're making
Rust and Go's lack of stack traces are basically equivalent in that you need to call an additional function to add the stack context to the error result. For go you use fmt.Errorf, in Rust you use .context from anyhow (bad practice in many contexts IMO) or .inspect_err + log. It's rather unfortunate that neither has an easy way of capturing a line number + file easily and appending it to the context. Go could easily do it, I think. Oh well.
I agree that Go should really have an analogue to Rust's `?`, but you can't really do that in a sane way without sum types to represent your conditions. The very multiple-return style error propagation makes it impractical to do.
IMO Go should add really basic tagged unions and rework the stdlib to use them, but such a change would probably necessitate a v2.
RE: Golang v2, they clearly said they will not do it and will double down on backwards compatibility with exceptions powered by env vars and/or CLI switches.
I'm fully aware of that.
> The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language
Go functions also accept a tuple on input. While theoretically you could pass an error, or a pointer to an error for assignment, it is a stretch to claim it is for errors.
Yes exactly, that rather useless feature just makes the whole thing even weirder.
I prefer the structure of Rust errors as it’s fully typed, I don’t like that you can chain them though. It’s a cool feature but it leaves you with some of the same issues exception handling does when the freedom is used “wrong”.
Exceptions are sum types, they just have different syntactic sugar.
Checked exceptions may be implemented as a sum type. Traditional exceptions are more likely to be a single type that wraps up a context object alongside stack trace information.
And different control flow, and different or sometimes non-existent types (Java's throws).
Not really. Exceptions usually imply unwinding the stack, and the ability to catch at any point throughout the callstack. Result types are just 'dead' data.
These are fully equivalent in outcome, though often not low-level implementation. You can use try...catch (called panic...recover in Go) to pack a normal and abnormal return case into the equivalent of a Result<> type. Or just pass an abnormal Result<> back to the caller to manually unwind a single "layer" of the call stack.
> These are fully equivalent in outcome
They are so different in DX, ergonomics, implementation and traceability that I'm not sure this is true other than in the most abstract sense
There is some DX similarity between checked exceptions and Result types.
Because the compiler will fail if you don't explicitly mention each possible exception.
But checked exceptions are coming out of style: They're unchecked in C#, and frameworks like Spring Boot in Java catch all checked exceptions and rethrow them as Spring Boot flavored unchecked ones.
For unchecked exceptions and Result types:
The DX is very different in one critical way:
With Results you constantly have to differentiate between error and ok states, before you proceed. With unchecked exceptions you generally assume you're always in an ok state. It's equivalent to wrapping your whole function body in 'try { ... } catch (Exception e)'. And you can get that with Result types in Rust by using '?' and not worry about doing something half-way.
Ultimately: Are you a happy-path programmer?
Having programmed for over 30 years, including nearly a decade of C#, I would say exceptions are one of the worst ideas in all of programming.
They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
Even junior engineers have a trivial time debugging most go errors, while even experienced principles struggle with figuring out the true cause of a Java exception.
Well, I've also programmed for over thirty years and I wouldn't use a language without exceptions and even wrote a whole essay defending that position:
https://blog.plan99.net/what-s-wrong-with-exceptions-nothing...
> Even junior engineers have a trivial time debugging most go errors
Not my experience at all. I had to do this once. An HTTP request to a production server was yielding a 400 Bad Request with no useful information for what was bad about it. No problem, I'll check the logs and look at the source code. Useless: the server was written in Go and the logs had no information about where the error was originating. It was just getting propagated up via return codes and not logged properly. It ended up being faster to blackbox reverse engineer the server. In a language with exceptions there'd have been a stack trace that pinpointed exactly where the error originated, the story of how it was handled, and the story of how the program got there.
Diagnosing errors given stack traces is very easy. I've regularly diagnosed subtle bugs given just logs+stack trace and nothing else. I've also had to do the same for platforms that only have error codes that aren't Go (like Windows). It's much, much harder.
> Diagnosing errors given stack traces is very easy.
This is the most important aspect of exceptions in my view.
The line that threw the exception isn't even the part of a stack trace that I find most interesting. The part that is most valuable to me when working on complex production systems are all of the call sites leading up to that point.
I remember in my junior years I wasn't a big fan of exceptions. A stack trace would make my eyes glaze over. I would try/catch at really deep levels of abstraction and try to suppress errors too early. It took me a solid ~5 years before I was like "yes, exceptions are good and here's why". I think a lot of this boils down to experience and suffering the consequences of bad design enough times.
Exception usability is definitely an area that needs work. If you work support for a dev platform for a while, it's a really common experience that people will post questions with a copy/pasted stack trace where the message+stack actually answers their question. You can just copy/paste parts back to them and they're happy. There's too much information and not enough tools to digest/simplify them and people get overwhelmed.
Still, better to have too much data than too little.
> Useless: the server was written in Go and the logs had no information about where the error was originating. [...] In a language with exceptions there'd have been a stack trace that pinpointed exactly where the error originated
Go has support for exceptions, not to mention providing runtime access to stack trace information in general. They are most definitely there if your application requirements necessitate, which it seems yours did. Unfortunately, language support only helps if your developers actually speak the language. Go in particular seems especially prone to attracting developers who are familiar with other languages, who insist on continuing to try to write code in those other languages without consideration for how this one might be different and then blame the technology when things don't work out...
I can't agree with you about C++ exceptions being worse than useless. Exceptional C++ is worth it. Safety isn't that hard with RAII and ScopeGuard..
In your map example, just add a scope guard that removes the just-added element using the returned iterator if the rest of the procedure doesn't succeed. It's no different in Java.
Many people against Go's error handling do not advocate for exceptions, but for a combination of an Either/Result type (for recoverable errors) and fully aborting (for unrecoverable errors).
Abort on the other hand is used WAY to liberally in Rust.
How I hate it, that every second function call can break my program when it's clearly not a "halt the world, it's totally unrecoverable that the user sent us nonsense" type.
Return a Result and get on with your life!
If a Rust function can panic, there's generally a non-panicking alternative. For example, `Vec` indexing has `vec[n]` as the panicking version and `vec.get(n)` as the version that can return `None` when there's nothing at that index.
I do wish this is something Rust had done better though - the panicking versions often look more attractive and obvious to developers, and that's the wrong way round. Vec indexing, IMO, should return Option<T>.
While that is true, there are clippy::indexing_slicing, clippy::string_slice for that:
One of my dream projects is creating a Rust stdlib based solely on panics for error handling. Sure, it'd be incompatible with everything, but that might be a feature, not a bug.
I think someone's already created this.
in the stdlib, yes. In 3rd party crates? Depends!
> never handled correctly
I’ve seen this argument, but if you look at real golang code and examples, it’s just a bunch of “if err <> nill” copy pasta on every line. It’s true that handling errors is painstaking, but nothing about golang makes that problem easier. It ends up being a manual, poor-man’s stack-trace with no real advantage over an automatically generated one like in Python.
Which could be solved in one swipe by adding a Result<T, Error> sum type, and a ? operator to the language. This is more a self-inflicted limitation of Go, then a general indictment of explicit error handling.
Exception and explicit on-the-spot handling are not the only two ways to handle failing processes. Optional/result types wrapping the are a clean way to let devs handle errors, for instance, and chaining operations on them without handling errors at every step is pretty ergonomic.
Rust's error handling evolution is hilarious. In the beginning, the language designers threw out exceptions --- mostly, I think, because Go was fashionable at the time. Then, slowly, Rust evolved various forms of syntactic sugar that transformed its explicit error returns into something reminiscent of exceptions.
Once every return is a Result, every call a ?, and every error a yeet, what's the difference between your program and one with exceptions except the Result program being syntactically noisy and full of footguns?
Better for a language to be exceptional from the start. Most code can fail, so fallibility should be the default. The proper response to failure is usually propagating it up the stack, so that should be the default too.
What do you get? Exceptions.
> what's the difference between your program and one with exceptions
Because errors as values are explicit. You're not forced to use ? everywhere; you can still process errors however you like, or return them directly to the calling function so they deal with it. They're not separate control flow like exceptions, and they're not a mess like Go's.
No, because you end up with a function coloring problem that way. A function that returns something other than Result has to either call only infallible code or panic on error, and since something can go wrong in most code, the whole codebase converges as time goes to infinity on having Result everywhere.
Yeah, yeah, you can say it's explicit and you can handle it how you want and so on, but the overall effect is just a noisy spelling of exceptions with more runtime overhead and fewer features.
I very much care about whether a function can fail or not, and I encourage all the function colouring needed to convey that.
As almost always, we programmers / software developers / engineers, forget to state our assumptions.
In closed-world, system-software, or low-level software you want to have your kind of knowledge about everything you call. Even more: can it block?
In open-world, business-software, or high-level software it is often impossible or impractical to know all the ways in which a function or method can fail. What you need then, is a broad classification of errors or exception in the following two dimensions: 1. transient or permanent, 2. domain or technical. Those four categories are most of the time enough to know whether to return a 4xx or 5xx error or to retry in a moment or to write something into a log where a human will find it. Here, unchecked exceptions are hugely beneficial. Coincidentally, that is the domain of most Java software.
Of course, these two groups of software systems are not distinct, there is a grey area in the middle.
> A function that returns something other than Result has to either call only infallible code or panic on error
...Or solve the problem. A library function that can be a source of issues and can't fix these issues locally should simply not be returning something that is not a result in that paradigm.
> since something can go wrong in most code
That is not my experience. Separating e.g. business logic which can be described cleanly and e.g. API calls which don't is a clear improvement of a codebase.
> the whole codebase converges as time goes to infinity on having Result everywhere.
As I said previously, it is pretty easy to pipe a result value into a function that requires a non-result as input. This means your pure functions don't need to be colored.
> Or solve the problem. A library function that can be a source of issues and can't fix these issues locally should simply not be returning something that is not a result in that paradigm.
People "solve" this problem by swallowing errors (if you're lucky, logging them) or by just panicking. It's the same problem that checked exceptions in Java have: the error type being part of the signature constrains implementation flexibility.
In my experience an unwrap, expect or panicking function is a direct comment in code review and won’t be merged without a reason explaining why panicking is acceptable.
One practical benefit of Rust’s approach that hasn’t been emphasised enough yet is the consequences of Option<T> and Result<T, E> being just values, same as anything else.
It means you can use things like result.map_err(|e| …) to transform an error from one type to another. (Though if there’s a suitable From conversion, and you’re going to return it, you can just write ?.)
It means you can use option.ok_or(error) or option.ok_or_else(|| error) to convert a Some(T) into an Ok(T) and a None into an Err(E).
It means you can .collect() an iterator of Result<T, E> into a Vec<Result<T, E>>, or (one of the more spectacular examples) a Result<Vec<T>, E> which is either Ok(items) or Err(first_error).
It’s rather like I found expression-orientation, when I came to Rust from Python: at first I thought it a gimmick that didn’t actually change much, just let you omit the `return` keyword or so. But now, I’m always disappointed when I work in Python or JavaScript because statement-orientation is so limiting, and so much worse.¹ Similarly, from the outside you might not see the differences between exceptions and Rust-style Result-and-? handling, but I assure you, if you lean into it, it’s hard to go back.
—⁂—
¹ I still kinda like Python, but it really painted itself into a corner, and I’ve become convinced that it chose the wrong corner in various important ways, ways that made total sense at the time, but our understanding of programming and software engineering has improved and no new general-purpose language should make such choices any more. It’s local-maximum sort of stuff.
Exceptions are values in C++, Java, and Python too. They're just values you throw. You can program these values.
As usual, I find that opposition to exceptions is rooted in a misunderstanding of what exceptions really are
Exceptions are values, but normal-value-or-exception (which is what Result<T, E> is) isn’t a value. Review my remarks about map_err, ok_or, &c. with the understanding that Result is handling both branches of the control flow, and you can work with both together, and you might be able to see it a bit more. Try looking at real code bases using these things, with things like heavily method-chained APIs (popular in JS, but exceptions ruin the entire thing, so such APIs in JS tend to just drop errors!). And try to imagine how the collect() forms I described could work, in an exceptions world: it can’t, elegantly; not in the slightest.
Perhaps this also might be clearer: the fuss is not about the errors themselves being values, but about the entire error handling system being just values.
The '?' operator is the opposite of a footgun. The whole point of it is to be very explicit that the function call can potentially fail, in which case the error is propagated back to the caller. You can always choose to do something different by using Rust's extensive facilities for handling "Result" types instead of, or in addition to, using '?'.
In most languages with exceptions:
• they may propagate automatically from any point in code, potentially breaking atomicity invariants and preventing forward progress, and have to be caught to be transformed or wrapped – Result requires an explicit operator for propagation and enables restoring invariants and transforming the error before it is propagated.
• they are an implicit side-channel treated in the type system like an afterthought and at best opt-out (e.g. "noexcept") – Result is opt-in, visible in the return type, and a regular type like any other, so improvements to type system machinery apply to Result automatically.
• try…catch is a non-expression statement, which means errors often cannot be pinpointed to a particular sub-expression – Result is a value like any other, and can be manipulated by match expressions in the exact place you obtain it.
Sure, if you syntactically transform code in an exception-based language into Rust you won’t see a difference – but the point is to avoid structuring the code that way in the first place.
> they may propagate automatically from any point in code, potentially breaking atomicity invariants and preventing forward progress
A failure can propagate in the same circumstances in a Rust program. First, Rust has panics, which are exceptions. Second, if any function you call returns Result, and if propagate any error to your caller with ?, you have the same from-anywhere control flow you're complaining about above.
Programmers who can't maintain invariants in exceptional code can't maintain them at all.
> try…catch is a non-expression statement,
That's a language design choice. Some languages, like Common Lisp, have a catch that's also an expression. So what?
> they are an implicit side-channel treated in the type system like an afterthought an
Non-specific criticism. If your complaint is that exceptions don't appear in function signatures, you can design a language in which they do. The mechanism is called "checked exceptions"
Amazing to me that the same people will laud Result because it lifts errors into signatures in Rust but hate checked exceptions because they lift errors into signatures in Java.
Besides, in the real world, junior Rust programmers (and some senior ones who should be ashamed of themselves) just .unwrap().unwrap().unwrap(). I can abort on error in C too, LOL.
Honest question: syntactically noisy as opposed to what? In the context of this post, which is a critique of Go as a programming language, for me this is orders of magnitude better than the "if err != nil {" approach of Go.
I'm not sure why you bring up Rust here, plenty of libs/languages use the Result pattern.
Your explanation of what bothers you with results seems to be focused on one specific way of handling the result, and not very clear on what the issue is exactly.
> what's the difference between your program and one with exceptions
Sometimes, in a language where performance matters, you want an error to be handled as an exception, there's nothing wrong with having that option.
In other languages (e.g. Elm), using the same Result pattern would not give you that option, and force you to resolve the failure without ending the program, because the language's design goals are different (i.e. avoiding in-browser app crash is more important than performance).
> syntactically noisy
Yeah setting up semantics to make users aware of the potential failure and giving them options to solve them requires some syntax.
In the context of a discussion about golang, which also requires a specific pattern of code to explicitly handle failures, I'm not sure what's your point here.
> full of footguns
I fail to see where there's a footgun here? Result forces you to acknowledge errors, which Go doesn't. That's the opposite of a footgun.
> Once every return is a Result, every call a ?, and every error a yeet
The Try operator (`?`) is just a syntax sugar for return. You are free to ignore it. Just write the nested return. People like it for succinctness.
Yeet? I don't understand, do you mean the unstable operator? Rust doesn't have errors either.
> what's the difference between your program and one with exceptions except the Result program being syntactically noisy and full of footguns?
Exceptions keep the stacktrace, and have to be caught. They behave similar to panics. If panics were heavy and could be caught.
Rust errors aren't caught, they must be dealt with in whatever method invokes them. Try operator by being noisy, tells you - "Hey, you're potentially returning here". That's a feature. Having many return in method can both be a smell, or it could be fine. I can find what lines potentially return (by searching for `?`).
An exception can be mostly ignored, until it bubbles up god knows where. THAT IS A HUGE FOOTGUN. In Java/C# every line in your program becomes a quiet return. You can't find what line returns because EVERY LINE CAN.
But with Exceptions you can easily implement multiple return types in e.g. Java ;)
I shocked my Professor at university with that statement. After I started laughing, he asked me more questions... still went away with a straight A ;D
As you should, it shows a deeper insight in the language beyond the base course material and out of the box (and cursed) thinking.
When I took the compiler course at university, the professor would have a new coursework theme every year, and the year I took the course, the coursework compiler was exception-oriented. So exceptions were the only control flow mechanism besides function calls. If/else, while, return were all variations of throw.
To me this proved that there's nothing inherently wrong about exceptions.
It's how you structure your code and the implied assumptions you share with your colleagues.
Some people are way too optimistic about their program will actually do.
Happy path programming.
Sounds interesting - any link to the control flow implementation online?
Or how does one optionally throw and exception without an "if" statement? What's the "base" exception call?
Ie if "if" is implemented via exceptions, how do exceptions get triggered?
And is "while" done via an "if exception" and recursion? Or another way?
In OCaml, that's actually a common use for exceptions, as they let you unwind extremely quickly and carry a result (helped by the fact that OCaml doesn't have finally or destructors).
I agree about implicit exceptions, but I think that there is a sweet spot with explicit exceptions like Swift (and maybe Java): where you cannot not-handle one, it is part of a function's signature, and the syntax is still compact enough that it does not hurt readability.
Is “goto” just used to mean “bad and evil” here? Because exceptions are not a goto anymore than a return is a goto. The problem with goto is it can jump to any arbitrary place in your code. Exceptions will only go to catch-blocks up the call stack, which presumably you have written on purpose.
> Because exceptions are not a goto anymore than a return is a goto
Not true at all
* goto goes to a specific hard-coded address
* return looks up the previous address from the stack and goes there
* exceptions are a complex mess that require branching logic to determine where to resume execution
Hard disagree. Exceptions are actually good. They make code clear and errors hard to ignore. I've written a ton of code over decades in both exceptional and explicit-error languages and I'll take the former every day. There's no function color problem. No syntactic pollution of logic with repetitive error propagation tokens.
Also, exception systems usually come with built in stack trace support, "this error caused by this other error" support, debugger integration ("break the first time something goes wrong"), and tons of other useful features.
(Common Lisp conditions are even better, but you can't have everything.)
You can't just wave the word "goto" around as if it were self-evident that nonlocal flow control is bad. It isn't.
> And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
That's not been my experience at all. Writing exception safe code is a matter of using your language's TWR/disposable/RAII/etc. facility. A programmer who can't get this right is going to bungle explicit error handling too.
Oh, and sum types? Have you read any real world Rust code? Junior developers just add unwrap() until things compile. The result is not only syntactic clutter, but also a program that just panics, which is throwing an exception, the first time something goes wrong.
Many junior developers struggle with error handling in general. They'll ignore error codes. They'll unwrap sum types. They might... well, they'll propagate exceptions non-fat ally, because that's the syntactic default, and that's usually the right thing. We have to design languages with misuse in mind.
> Have you read any real world Rust code? Junior developers just add unwrap() until things compile.
If you really don't like unwrap[1], you can enable a linter warning that will let you know about its uses to flag it during code review. You know exactly where they are and when they happen. Exceptions are hidden control flow, so you rely on documentation to know when a function throws.
> Writing exception safe code is a matter of using your language's TWR/disposable/RAII/etc. facility. A programmer who can't get this right is going to bungle explicit error handling too.
Rust has RAII, so you don't have to worry about clean-up when returning errors. This is a Go problem, not Rust.
[1] https://blog.burntsushi.net/unwrap/
Bah, no, I hated that you had to wrap basically every code block in a try/catch in Java, because the underlying lib could change and suddenly throw a Runtime-Exception.
At the same time Checked Exceptions were a nightmare as well, because suddenly they were part of the contract, even though maybe wrong later.
> the underlying lib could change and suddenly throw a Runtime-Exception.
And what would you do in that case? Since this is a future change your existing code presumably wouldn't know what else to do but throw its own exception, so why not just let that one propagate?
Checked exceptions are more trouble than they're worth. That doesn't make exceptions in general bad.
Not having checked exceptions is a huge problem, because then you never know when something might throw and what it might through, and in the .NET world the documentation on that is pretty awful and absolutely incomplete.
But then over in Java world, your checked exception paradise (which it of course isn't because the syntax and toolkit for managing the things is so clunky) is easily broken by the number of unchecked exceptions which could be thrown from anything at any time and break your code in unexpected and exciting ways, so not only do you have to deal with that system you also don't get any assurance that it's even worth doing.
But this doesn't actually mean checked exceptions are a bad idea, it means that Java didn't implement them very well (largely because it also has unchecked exceptions, and NullPointerException is unchecked because otherwise the burden of handling it would be hideous, but that comes down to reference types being nullable by default, which is a whole other barrel of pain they didn't have to do, and oh look, Go did the same thing wooo).
> in the .NET world the documentation on that is pretty awful and absolutely incomplete.
Depends on the area you look at. Language documentation is pretty good and so is documentation for the standard library itself. Documentation for the frameworks can be hit or miss. EF Core is pretty well documented and it’s easy to find what to look for usually. GUI frameworks are more of a learning curve however.
FWIW many in Java community consider checked exceptions to be a mistake. While I don’t find writing code that has many failure modes particularly fun with exception handling - Rust perfected the solution to this (and the Go way is visually abrasive, no thanks), I don’t think it’s particularly egregious either - Try pattern is pretty popular and idiomatic to use or implement, and business code often uses its own Result abstractions - switch expressions are pretty good at handling these. Personally, I’d write such code in F# instead which is a recent discovery I can’t believe so few know how good it is.
What does make exceptions bad in my opinion (and shared by Go developers?) is a few things:
1. Exceptions are expensive (at least in Java / C#), as they generate a stack trace every time. Which is fine for actually exceptional situations, the equivalent of `panic()` in Go, but:
2. Exceptions are thrown for situations that are not exceptional, e.g. files that don't exist, database rows that don't exist, etc. Those are simple business logic cases. The workaround is defensive coding, check if the file exists first, check if the row exists? that kind of thing.
3. The inconsistency between checked and unchecked exceptions.
4. Distance - but this is developer / implementation specific - between calling a function that can throw an error and handling it.
But #2 is the big one I think. Go's error handling is one solution, but if it's about correct code, then more functional languages that use the Either pattern or whatever it's called formally are even better. Go's approach is the more / most pragmatic of the options.
> e.g. files that don't exist, database rows that don't exist, etc. [...] The workaround is defensive coding, check if the file exists first, check if the row exists?
Ugh NO. Please don't. You should never "check if the file exists first". It can stop existing between your check and your later attempt at opening the file (the same with database rows). That can even lead to security issues. The name for that kind of programming mistake, as a vulnerability class, is TOCTOU (time-of-check to time-of-use).
The correct way is always to try to do the operation in a single step, and handle the "does not exist" error return, be it a traditional error return (negative result with errno as ENOENT), a sum type (either the result or an error), or an exception.
Totally agreed, but as the previous poster wrote:
an exception is meant for EXCEPTIONAL behavior.
So it may be that the file access throws an exception but generally, I wouldn't agree.
As said, I don't like the wrapping of about everything with try/catch
Sure, you can only do it way up the stack, but that's not enough quite often.
If you can only do it all the way up, I find it ergonomic.
Maybe I should experiment more with catch unwind in Rust.
> Oh, and sum types? Have you read any real world Rust code? Junior developers just add unwrap() until things compile.
Junior developers will write suboptimal code in any language. So I'm not sure what your point is.
Without fail, every single person I’ve seen rave about go’s error handling compares it only to exceptions as if that’s the only alternative.
On the flip side I have yet to find a person who’s familiar with sum types (e.g., Maybe, Option, Result) that finds the golang approach even remotely acceptable.
Here, you've found me.
I don't LIKE it, but it's acceptable.
I have been working with Rust since 2015 (albeit not professionally, but a lot of side projects) and love it.
But I also dabbled into go the last couple of months and while it has its warts, I see it as another tool in the tool-belt with different trade-offs.
Error handling is weird, but it's working, so shrug
Sorry, balanced opinions are not welcome in discussions about favourite programming languages.
I think it’s because Go is an alternative to Java and C# more so than an alternative to Rust. It is for me at least. As I said, Rust isn’t seeing any form of real world adoption in my region while Go is. Go isn’t replacing C/C++ or even Python though, it’s replacing Typescript, C# and Java. Now, there are a lot of good reasons as to why Go shouldn’t be doing that, a lot of them listed in the article, but that’s still what is happening.
As I pointed out I think Rust does it better with its types error handling. That isn’t too relevant for me though as Rust will probably never seem any form of adoption in my part of the world. I think Zig may have a better chance considering how interoperable it is with C, but around here the C++ folks are simply sticking with C++.
> Now, there are a lot of good reasons as to why Go shouldn’t be doing that
I disagree. Typescript, C# and Java are terrible languages (as are Python/Ruby/etc. in other ways). Golang is bad by OP's standards but there's nothing wrong with it gaining ground on those languages.
Besides it's also easier to convert a codebase to Rust from Golang than Typescript or C#/Java.
Rust and C# have far more overlap than Go could ever hope for. Go is limited (and convoluted sometimes due to "solutions" devised to cope with it) so it is easily expressible in languages with better type systems and concurrency primitives.
I dislike sum-type based error handling. It is annoying syntactically and only really doable with lots of high-level combinators, which in turn hinder debuggability.
Have you tried the approach that Zig has, or the approach that Rust has? They are easy to debug and do not use any crazy stuff, just simple syntax like `try x()` (Zig) or `x()?` (Rust)
You hit the main gripe I have with Go, its types system is so basic. I get people raving type-correctness of Go when they come from Python but the type system in Go is simply pre-historic by modern day standards.
I feel that the future for Python people who want type safety will eventually be TypeScript on nodejs. Go was intended as an alternative to C++. It seems that in reaction to the ungodly complexity of C++, the creators wanted to avoid adding language features as hard as possible. If the user could work around it with a little extra verbosity, it'd be ok. I feel they removed too much and maybe not the right things.
Go’s type system is not even impressive compared to python’s.
Do you have a pydantic equivalent in go? Also modern typing in python is starting to be OK to be honest (well, if you consider typescript typing OK), so it isn't really a knock on Go :)
> Do you have a pydantic equivalent in go?
I've been working on one [1].
But gosh, does go make it hard.
[1] https://github.com/pasqal-io/godasse
Well I was comparing to python codebases before they added type annotations
Which, sadly, is still the case of too many dependencies.
While I much prefer Python as a language, Go wins against Python by having a fresher ecosystem, with a higher baseline for type safety. Still pretty low with respect to Rust or mypy/pyright with highest settings, but much better than any of the Python frameworks I've had to deal with.
That’s to be expected since it is marketed towards beginner and casual programmers.
I don't agree that that's what it's marketed towards, but it was designed with those in mind. That said, experienced developers can enjoy it too, as code is just a means to an end and code complexity or cleverness does not make for good software in the broader sense of the word.
It's a Google solution to Google scale problems, e.g. codebases with millions of lines of code worked on by thousands of developers. Problems that few people that have an Opinion on Go will ever encounter.
FWIW... WebAssembly has Option and Result, and adapters for Go.
What do you mean by this? WebAssembly is a low level bytecode which only defines low level types. WebAssembly doesn't "have" types any more than x86 "has" types right? Or have I missed something?
Ah, sorry. Shoulda said: WebAssembly component model.
Oh, I see. Thanks!
this part of error handling is pure religion. it goes even against one of the most basic go tenents. that code should be easy to read not write. Try reading and understanding the logic of a particular method where 75% of the lines are error noise and only 25% are the ones you need to understand what the method does. yes it's noise because whenever read a codebase for the first time you are never interested on the the error edge case. first glance readability needs to tell you what you are trying to accomplish and only after what you are doing to make sure that is correct.
on this point go's error handling is a massive fail. Notice that I'm not saying explicit error handling is bad. I'm saying the insistence that error handling needs to be implemented inline interleaved with the happy path is the problem. You can have explicit error handling in dedicated error handling sections
Do you have examples for the latter?
the most basic example was the declined proposal https://github.com/golang/proposal/blob/master/design/32437-...
Some people didn't like the "try" keyword it reminded them too much of exceptions, some people didn't like that they couldnt see a return inline (which was the purpose of the proposal in the first place).
it's not that there are no solutions. the main problem is the go team's insistence to have "one true way" (tm) of doing something and unfortunately this gap between people who want to see every return inline and people who want to see the clean solution separate from the error handling is not something that can be bridged by technical means. the only solution is to implement both ways and lets see which one wins.
I would certainly argue against the claim that explicit error handling is far overkill.
Where I agree: It forces you to think about all of the possibilities your code might generate. (This is more of a C question than it is with other languages)
However, when abstracting blocks of code away, you don't always need to handle the error immediently or you may want to handle it down the stack.
You're giving up a lot of readability in order for the language to be particular.
> It forces you to think about all of the possibilities your code might generate.
Except it doesn't actually. You can totally just ignore it and pretend errors don't exist. Lack of sum types/Result, and pointers as poor mans optional, really hinder's Go's error handling story.
> It forces you to think about all of the possibilities your code might generate
I’ve seen way too much Go code which never even tested the err value to believe that until something like errcheck is built in to the compiler.
I do agree that this is a plus for the explicit model but that’s been a better argument for Rust in my experience since there’s better culture and tooling around actually checking errors. I’m sure there are plenty of teams doing a good job here, but it always felt like the one lesson from C they didn’t learn well enough, probably because a language created by experts working at a place with a strong culture of review doesn’t really account for the other 99% of developers.
Rust handles this much better.
Error handling is still explicit, but it gives you the tools needed to make it less tedious.
For me, the issue with error handling is that while errors are explicitly stated, they are often poorly handled. Rarely have I seen the handling of multiple reasons for why an error might occur, along with tailored approaches to handle each case. This is something very common in older languages like Python or Java
As a regular Go user, I agree with this take. Though the tools exist, error wrapping and checking (with errors.Is and so on) is actually pretty rare in my experience.
Positive example of good and appropriate usage here: https://github.com/coder/websocket/blob/master/internal/exam...
This is down to developer style and agreements though; Go has typed errors and a set of utilities to match them [0]. Not using those is a choice, just like how in Java you can just `catch (Exception e)` after calling a dozen methods that might each throw a different exception.
[0] https://pkg.go.dev/errors
Interestingly, every time (and I mean _every_ time) that I've tried to use `errors.As` on errors raised by lib code, I found out that the lib just went with "nah, I'm just going to use `errors.New` or `fmt.Errorf`", which makes the error impossible to match.
So... I'd say that this is a fumble in the design of Go.
I see a lot of people say this about exceptions, and I don't have that problem. The exception bubbles up the stack until something catches it. Ok it's a different code path, but it's a very simple one (straight up). So you either catch the exception nearby and do something specific with it, or it bubbles up to a generic "I'm sorry there was a problem please try again later" handler.
Honestly makes me wonder what I'm missing. Maybe it's because I don't deal with state much? Do the problems start to mount up when you get into writing transaction locks, rollbacks etc? But I don't see why you wouldn't have the same problems with Go's mechanism.
Hoping to gain enlightenment here.
[copied from a comment below]: They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
Maybe this is it? I prefer a "fail early and often" approach, and I tend to validate my data before I perform operations on it. I don't want to attempt recovery, I want to spew log messages and quit.
> They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about.
I think that’s misdirected but illustrates the emotional reasons why people develop a negative impression of the concept. Usually it means someone had bad experiences with code written in a poor culture of error handling (e.g. certain Java frameworks) and generalized it to “exceptions are bad” rather than recognizing that error handling isn’t trivial and many programmers don’t take it seriously enough, regardless of the paradigm. As a simple example, C and PHP code have had many, many security and correctness issues caused by _not_ having errors interrupt program execution where the users would have been much better off had the program simply halted on the first unhandled error.
If you write complex programs with lots of mutable shared state, yes, it’s hard to reason about error recovery but that’s misattributing the problem to the mechanism which surfaced the error rather than the fact that their program’s architecture makes it hard to rollback or recover.
> Go is seeing adoption that no other “new” language has exactly because of its simplicity
Yes - for me, the simplicity is essential. As a part-time programmer, I don't have months to spend learning C++ or Rust.
If my project needs to compile to small(-ish) standalone binaries for multiple platforms (ruling out Python, Ruby, Java, C#, etc) what simple alternative language is there? Plain C?
Basic Rust doesn’t take months to learn, especially when you’re not trying to do things like distributing crates to other people. I found the compiler to be enough more helpful than Go’s to make them roughly time-equivalent for the subset of common features, especially for a simple CLI tool.
C# can compile standalone binaries for multiple platforms.
It's actually very rare that it should be the caller who has to handle the errors.
Go, however, forces you to spread your error handling over a thousand little pieces with zero overview or control of what's happening.
Rust eventually realised this and introduced try! and ? to simplify this
More importantly, Rust has the notion of a result type and it is designed to be both generic and composable.
A problem I often face in Go and TypeScript code is code that ignores errors, often unintentionally. For instance, many uses of JSON.parse in TypeScript do not check for the SyntaxError that may be thrown. In Go, it is common to see patterns like
This pattern exists to tell the reader "I don't care if this method returns an error". It allows one to avoid returning an error, but it also stops the caller from ever being handle to the error.Also, the position of the error matters. While the convention in the stdlib is to return errors as the final value, this isn't necessarily followed by third party code.
Similarly, errors are just an interface and there is no requirement to actually handle returned errors. Even if one wants to handle errors, it's quite awkward having to use errors.As or errors.Is to look into a (possibly wrapped) chain of errors.
The benefit of Rust's Result<T, E> is that
- position doesn't matter
- there is strong, static type checking
- the language provides operators like ? to effortlessly pass errors up the call stack, and
- the language provides pattern matching, so it's easy to exhaustively handle errors in a Result
The last two points are extremely important. It's what prevents boilerplate like
and it's what allows one to write type-safe code rather than guess whether errors.As() or errors.Is() should be used to handle a returned error.I am pretty sure if it were for the Typescript creators they would not allow exceptions in the language, but they had to work within the confines of Javascript. Heck they even refused to make exceptions part of the type-system.
It is unfortunate that many of Typescript developers still rely on throwing exceptions around (even in their own typescript code). Result types are totally doable in Typescript and you can always wrap native calls to return result types.
Why would you "check" for TypeError being thrown? Just let exceptions in general propagate until they reach one of the few places in the program that can log, display, crash, or otherwise handle an exception. No need to "check" anything at call sites.
90% of the criticism of exceptions I see comes from the bizarre and mistaken idea that every call needs to be wrapped in a try block and every possible error mentioned.
> Rust eventually realised this and introduced try! and ? to simplify this
That was prototyped around Rust 0.4, so I wouldn't say "eventually" :)
Unsure if this is the right place to ask, but this conversation inspires me this question:
Is there in practice a significant difference between try/catch and Go's "if err" ? Both seem to achieve the same purpose, though try/catch can cover a whole bunch of logic rather than a single function. Is that the only difference ?
Try/catch can bubble through multiple layers. You can decide/design where to handle the errors. If you don't `if err` in Golang, the error is skipped/forgotten, with no way to catch it higher up.
You can decide not to catch a thrown exception, it travels upwards automatically if you don't catch it.
I think that's the biggest difference.
With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards. If you do, what kind of context would the caller want from this piece of code, you can add that too.
> With Go you need to specifically check the errors and intentionally decide what to do, do you handle it right there or do you bubble it upwards.
Is this really all that interesting or worth the LOC spent on error handling when 99.9999% of the time in practice it’s just bubbled up?
And any “context” added is just string wrapping. Approximately nobody types golang errors in a way that lets you programmatically know what went wrong, to be able to fix it in-line.
I think I would be more empathetic to the arguments defending golang here if I’d ever worked or seen a project where people actually handled errors instead of spending 2/3 of their time writing code that just punts on any error.
I'd argue that at least checked exceptions also require a conscious decision from you. You either need to add the Exception type to your throws clause, or your catch clause.
Compared to Go, this is actually better because the type system can tell you what kind of errors to expect, instead of just "error".
> I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up.
I kind of see your point. In this very moment, it doesn't matter whether I agree. What I don't understand, though, is why (typically) people who abhor exceptions are among the fiercest defenders of garbage collection, which does add a “magic” and uncontrollable layer to object destruction.
Personally, having learned to love RAII with C++, I was shocked to discover that other languages discarded it initially and had to add it in later when they realized that their target developers are not as dummy as those choosing Golang.
Different kind of magic. Needing to account for every single line of code being able to throw an exception is very mentally taxing, whereas the existence of a GC removes mental load of needing to account for every single allocation.
How does RAII works in concurrent systems ? It seems to me you need to add compile-time object lifetime evaluation (as in rust) which so far incurs a high toll on language complexity.
The exact same way it works in Rust. C++'s RAII works the same as Rust's Drop trait. The object is released when it goes out of scope, and if it's shared (e.g. Arc), it's released when the last reference to it drops.
When I started trying to teach myself Rust, the error handling story fell apart on me very quick.
Like as soon as I wanted to try and get sensible reporting in their, suddenly we were relieving libraries, adding shims and fighting mismatched types and every article was saying the same thing: haha yeah it's kind of a problem.
I'm very, very unsold on explicit error handling compared to exceptions for practical programming. The number of things which can error in a program is far larger then those that can't.
I felt the same but after switching to anyhow and thiserror in pretty much every Rust project I work on I find it quite painless. It's not ideal to rely on crates for a core language feature but I never find myself fighting error types anymore. Have you tried those crates? Do you still hold that opinion?
You don’t need crates for it, anyhow is basically a better Box<dyn Error>, if you just want the error signal you can use that. The main thing missing from the stdlib fur this use case is I don’t think there’s anything to easily wrap / contextualise errors built in.
The problems you're describing don't exist in go. There is exactly one standard type that is used by everyone, at least in public API's, you can always just return the error to the caller, if you don't want to handle it in place. The main difference with exceptions in my practice is the fact that it's a lot easier to mess up, since it requires manual typing. This is probably my main problem with everything being as explicit as possible: it requires people to not make mistakes while performing boring manual tasks. What could possibly go wrong?
The drawback, on the other hand, is that all the Go code I've read (including the stdlib and all the dependencies of my current project) is using `fmt.Errorf` or `errors.New`, which means that you can't use `errors.As`, which means that you generally cannot handle errors at all.
I think that sort of nails it: the problem with errors as values is errors become part of the type signature and put under user control, and the user can't really be trusted with that power.
Even the simplest functions tend to have error states - i.e. floating point math can always wind up handing back NaN.
So where I end up is, the default assumption is every function is effectively of a type MightError(T)...so why even make us type this? Why not just assume it, assume the unwrap handling code, and so you basically wind up back at try-catch exception handling as a syntactic sugar for that whole system.
Rust and Go are very different and I feel people want a middle ground that just doesn't exist currently.
A garbage collected relatively simple language that compiles into a statically linked binary but has a type system similar to rust, rest types etc.
Syntactically, Gleam and Kotlin come somewhat close but not really. I like Rust but I do believe it is too complicated for many people who are capable of creating something but are not CS grads nor working as programmers. If you're only gonna use the language every once in a while you won't remember what a vtable is, how and when things are dropped etc. I understand that "the perfect language" doesn't exist but I think both Go and Rust brought amazing things to the table. I can only hope someone takes inspiration from both and creates a widely usable, simple programming language.
Kotlin is interesting as a middle ground, but I still find it much less productive than Go for most tasks, and unsuitable for tasks where you'd reach for Rust.
In practice, Kotlin is extremely complicated, and you end up spending time being clever. There are 1000 ways to do things. Operator overloading. Proxies. Properties. Companion objects. Exceptions AND result types...
The build system (assuming you use Gradle) is tantamount to torture for anyone used to "go build".
The coroutines APIs feel simultaneously more complicated and yet more restrictive than Goroutines. More structured but less flexible and more effort to use.
Access control feels awkward. There's no way to make a type package-private -- it's file-private or available to the whole module. This leads to either a larger API surface than desired, or the inability to break up complexity into multiple files.
Kotlin/Native and Kotlin/JVM really are two different beasts too.
Kotlin/JVM is mature, but then you are running on the JVM, so that cuts out a whole class of use cases you might bust out Rust for.
There is a very weak ecosystem for Kotlin/Native, and it's poorly documented. There are some scary bugs in the bug tracker.
You can't publish source-only libraries for Kotlin/Native either, so you need a complex CI setup to build binaries for every OS and arch under the sun. Or just don't publish libraries at all, which probably feeds in to the weak ecosystem...
Imho, most features of Rust that people would like to see in Go would still fit into the "concept" of Go. Just like they added generics, they could add just three things: A generic container for errors (Result), one for saner Nil handling (Optional) and a small sprinkling of syntax sugar to make these comfortable to work with (something like an elvis operator equivalent).
Go has the one big advantage that is almost solely responsible for it's success: It was created and directly used by a giant company that could afford to create amazing tooling around it and develop great opensource libraries for it. Already being in use and having libraries feel like the biggest determinants of a languages success.
(author here) in which ways does Gleam come short of that? Because I'm also looking for that middle ground and I was very curious to get a look at Gleam.
IMHO it's just that it's a beam VM language, which is a fatter runtime/ecosystem than is really needed to achieve the goal stated above can bring it's own bag of problems (but also it's own superpowers).
Also to be productive you have to utilize the rest of the erlang ecosystem, so at least some superficial knowledge in elixir & erlang is helpful for for some use-cases.
Syntactically I actually don't think it's that for off, but I dunno what GP was thinking, maybe that it leans more into functional patterns & sugar for those whereas rust/go can also be used in a procedural style. (Though at least personally I am using way more functional patterns in rust than I expected)
> A garbage collected relatively simple language that compiles into a statically linked binary but has a type system similar to rust, rest types etc.
Swift.
If they'd drastically improved their tooling then yes.
But sadly it's not that easy to create a statically liked binary in swift. The last time i did it it also included the whole runtime, and the resulting "hello world" binary was 50mb large. Annoying at least.
For years I wished they got their stuff together, but at this point I'd be very suprised. They probably have too much technical dept already due to the support of XXX edge cases Apple need for iOS/MacOS development.
> relatively simple
I think Inko (https://inko-lang.org/) has the potential to be that language with some tooling/adoption/maturation
There is Crystal and Nim. With especially Nim, there is GC and generates c in the end.
I love Crystal, but the lack of a proper LSP and tooling makes it hard to just jump in and adopt for bigger projects.
> A garbage collected relatively simple language that compiles into a statically linked binary but has a type system similar to rust, rest types etc.
You just described Ocaml and ReasonML (which is Ocaml with Go-like syntax).
Last time i tried to install ocaml on windows few months ago i failed to do so, it's a well known thing that it is not 100% supported on windows therefore it wont have the adoption that go/rust has (as it's been the case forever now)
This doesn't count because the idioms are very different from Go and Rust. And I suspect there's a high learning curve for features like Functors.
If you check the Wikipedia page for OCaml to find out where it gets used, you'll see why it's ocaML. That is, you'll notice that it's mostly a MetaLanguage, or a language for writing other languages. The same observation applies to other languages in the ML family.
> A garbage collected relatively simple language that compiles into a statically linked binary and has a [good] type system
Yeah! Pattern matching too. What are currently available languages closest to this? AFAIK, Gleam relies on a virtual machine, but otherwise seems promising.
Stretching 'currently available' a little, there's Roc lang [1]. Though still in development, you can use it for small personal projects. Another caveat is that it's a functional language, which could potentially hinder its wide adoption
[1] https://www.roc-lang.org/
Try F# with the new AoT compilation option and publish single file switch.
> A garbage collected relatively simple language that compiles into a statically linked binary but has a type system similar to rust, rest types etc.
So OCaml then (ocamlopt to do native code compilation)
> people want a middle ground that just doesn't exist currently
https://borgo-lang.github.io/
Rust syntax, compiles to Go.
C# and F# will be by far the closest. Other options lack sufficiently good type system or tooling to match either of the two.
Compile to static native binary with 'dotnet publish -p:PublishAot=true' (or add this property to .csproj to not specify on each publish). In the case of F#, you will need to use Console.* methods over 'print*' because print has unbound reflection inside for structural output on "%A" format specifier (it will work most of the time but negatively impacts binary size and causes the compiler to complain).
I can especially recommend F# as "easier more business-focused Rust alternative" because it is expression-oriented, has discriminated unions, full HM type inference and gradual typing is a joy to work with. Data analysis and domain modeling are very pleasant to do in it too.
For systems programming C# is going to be the option to use - it will give you great concurrency primitives, fast (sometimes even zero-cost) native interop, smaller than Go native binaries and a lot of low-level APIs including portable SIMD. Go is often poorly suited for these tasks or can't do them at all (at least without workarounds). There are many new high-performance libraries focused on this domain as .NET gains popularity in non-gaming communities in this area. And of course you benefit from a huge existing ecosystem and won't have to do the all the heavy lifting by yourself unlike in niche languages suggested in sibling comments.
I wonder what makes someone go such a great length to bash a language, any language. I say bashing, because even the few valid points in the post are not written in a constructive style. After all is there a language that can't be criticised?
Is the post written to make one feel better having a failed a project the language? (It's not me, it's the language) Or is it the failure to understand that not everyone thinks / works the same and what one finds unacceptably bothersome, others barely notice? Languages that do not cater for a real need would likely vanish on their own, they rarely need help.
As for Go, despite the differences compared to "more sophisticated" languages it works brilliantly for the projects I've encountered. I hope the author is not forced to work with Go though. For the rest of the community, we keep churning out services, giving our feedback to the Go team and seeing the slow evolution of the language without breaking our stuff in production.
> what makes someone go such a great length to bash ...
"""
Inherent complexity does not go away if you close your eyes.
When you choose not to care about complexity, you're merely pushing it onto other developers in your org, ops people, your customers, someone. Now they have to work around your assumptions to make sure everything keeps running smoothly.
And nowadays, I'm often that someone, and I'm tired of it.
"""
> [Go] works brilliantly for the projects I've encountered.
Of course, C, C++, PHP and JavaScript works too! Of course many many many things "work" in our world. Of course just adding one more lane works too, of course police states work too!
Yet something else would work even more brilliantly?
It's just some person's blog and they're having a rant. It's okay, it doesn't have to be that deep.
I would guess the 'why' is because OP feels like they have an opinion that they don't feel is sufficiently represented 'out there'. Indeed, as a not-a-fan-of-go, in 2022 I was confused at go's popularity because it always felt to me to have some pretty glaring shortcomings that were seemingly ignored.
Note that people don't really write big blog posts about PHP being a bad language (anymore?) because that's been done to death.
Perhaps not everyone likes boring clinical reviews and some people like ones that have a bit of passion and humour in them?
Just because this blog post isn't written in a way you like doesn't mean it doesn't have value to others.
One very subjective, very irrational factor for my borderline hate for Go is that for years the Go zealots gaslighted everyone about every single part of Go.
Anything that Go did, no matter if it was the most basic implementation or if other languages already did it (better), was essential, the best and only way to solve that issue.
Anything Go did not do was superfluous and downright a conspiracy by Big Complexity to keep us unenlightened Non-Goers addicted to the Syntax Sugar: Things like sane null handling, sane error handling, less boilerplate, generics, or not creating special cases for everything (generics and tuples) instead of developing one cohesive solution.
Even now, in this thread, the strawmanning continues: Error handling is brought up, and instead of admitting the unmistakable truth that Gos error handling could be much better (s. Rust), people bring up things like JavaScript. As if anyone criticizing Go that JavaScript was the pinnacle of error handling.
Yes, I've also grown tired of this toxic mindset, as well as the whole "idiomatic Go" dogma that is very often an excuse for poor engineering practices.
Yeah, this was/is a part big part of my frustration with the ecosystem too. It set a LOT of very problematic patterns in place in the beginning, and many of them are still not unwound.
Stuff like "Go doesn't need dependency injection because it's simple". I heard that literally dozens of times. The opposite is true! It's an even bigger pain without DI because the language is so simple! DI everything or make your users suffer!
Or a personal favorite: "Go doesn't need a package manager". We see where that went, and how much insanely better it is now that we have it.
Or errors. Without wrapping. Oh boy we're gonna pay for that for decades.
There's stuff to like in the language, but the zealots (especially early ones) really did not know which parts they were.
[dead]
I feel the "critique" is not very balanced, and I view judgements that are not balanced as weak, as everything in technology is about tradeoffs.
I of course come to a different conclusion: https://www.inkmi.com/blog/why-we-chose-go-over-rust-for-our...
Very common misrepresentation of any critique of Go. "You just don't get simplicity, you got them Java brainwormz"...
There are many examples in the article that point out the annoying inconsistencies in the language, those are the opposite of simplicity.
I love Rob Pike's presentations on Go, some of them were eye-opening to me. However, I just wish that the Go I see in practice would be much closer to the Go language that Go-fans describe in abstract.
Scheme is a simple language, Go just hides complexity until it blows up in the worst possible way. (Of course, most reasonable alternatives to Go are even worse from that POV. See Python, Ruby, JS etc.)
Go is not simple, it's easy.
The difference, for example is: Go invents an abstraction just for one use case, and just for the standard library/runtime itself, instead of taking the time to create a universal version of that abstraction. Generics for maps and lists, and tuples for error handling.
> Perhaps it's because I have experience, I love a simple language.
(fixed the statement to focus on your value, not age per se)
I love language ergonomics above all. Python wins. But for runtime bang-for-the-buck, Go wins.
The problem is that Go is not designed to be a simple language for it's users, it's designed to be a simple language to implement for it's maintainers.
In my opinion, a simple language should be highly consistent (have a few rules, but which are universal and consistent everywhere). Instead we have a language with weirdnesses, inconsistencies and workarounds all over the place.
A good example is type elision: it's available when declaring arrays, slices and maps, but not for structs. Allowing that would have several benefits in terms of readability, and also allow named parameters via anonymous struct arguments (which would greatly improve the design compared to the ugly workarounds that are currently used).
I wish the discourse that Go is a "simple" language would die.
Despite its veneer, once you start writing Go it quickly becomes apparent that it isn't simple. Hidden complexity and footguns are abundant (e.g., https://archive.ph/WcyF4).
It's nevertheless a useful language, and I use it quite a bit, but it's not "simple".
I have no idea why anyone would say it's not simple, it's super-simple. Learning how duck typing works with interfaces and how to use it is perhaps the only hurdle. In my experience, only certain BASIC dialects like VisualBasic are simpler.
I think the sticking point is what people mean when they say simple. To me, and likely to many saying Go isn't simple, simple is not a synonym for easy.
Go is easy, but it is not simple. For example, solving the problem of generics in a generic way from the start so the same problem can be addressed in the same way everywhere, would be simple, but maybe not (as) easy. Contrast that to giving the runtime/standard library a special exception with maps and lists. That's easy, but not simple. People used to literally use code generators or weird UTF-8 characters to fake generics, that's not remotely simple.
This 100%, I was just about to type a long rant up about this. There are so many weird parts of the language that took me forever to grasp, and in many cases, I still don't have an intuitive grasp of things.
And plenty of other examples that aren't in that article:
- You have a struct with an embedded interface. Does the outer struct satisfy the embedded interface? And can I type assert the outer struct into whatever embedded struct is fulfilling the inner interface?
- When should I pass by value and when should I pass by reference? Like I generally know when to choose which, but do I really know without performing a benchmark? And what about arrays? Should I store pointers in them? But it also seems that people just don't care and just randomly roll the dice on when to return a pointer or a value?
- Shorthand variable declaration. How does it work when you shorthand declare two variables but one of them already exists?
Don't both answering the questions, that's not the problem. The problem is that it's just not intuitive enough such that I'm confident I know the correct answer.
I am not going to answer the questions, but this is a very strange complaint, to be honest.
For example, passing by value/passing by reference is something covered immediately in the Go FAQ document once and for all. Everything is passed by value in Go, that is it. There should be no confusion at all. If you spend 15 minutes reading Russ Cox's post on the internals of the most common data types, you will also understand what data structures have implicit pointers under the hood.
Well yes obviously I know everything is passed by value, just like in literally every other popular language. I'm talking about the difference between pointer parameters/receivers vs value parameters/receivers.
Your thinking is too complex for Go. You might be better of with Rust.
Same about benchmarking, if you want and need the fastest code, or the best memory management, use Rust.
If you need something faster than Python in general but not the fastest, use Go.
But that's the thing right? Like I come from Java. In Java, we have objects. They are pointers. That's it. You don't get to decide on whether you want a pointer or a value (I guess primitives are an exception lol). But it was so simple!
And same in JavaScript. Everything is a pointer except primitives. That's it. End of story.
And I have written Rust too, and while the situation is definitely more complicated there, the guidance is extremely simple and straightforward: If the struct implements Copy, then it is very cheap to copy and you should pass by value. Otherwise, you should pass by pointer/reference.
And meanwhile in Go, I just see pointers and values being used seemingly interchangeably, and seemingly at random.
> but do I really know without performing a benchmark?
Not really. But that’s one of Rob Pikes rules [1], I think the intention is to write whatever is simplest and optimize later. The programmer doesn’t need to remember 100 rules about how memory is allocated in different situations.
[1] https://users.ece.utexas.edu/~adnan/pike.html
I mean it's a great idea, and I fully agree that I do not want to worry about memory allocation. So then why is `make` a thing? And why is `new` a thing? And why can't I take an address to a primitive/literal? And yet I can still take an address to a struct initialization? And why can't I take an address to anything that's returned by a function?
https://go.dev/ref/spec#Address_operators
It's easy not simple, but the consequence is that any complexity that other languages handles for you, in go gets forced onto the developer.
There's more stuff to think about, because the language is doing less for you.
Yes, like "Opening Brace Can't Be Placed on a Separate Line" (from your link).
Everyone can read Go code and understand what happens. There are some minor difficulties like func (*A) vs func (A).,
RiscV assembly is even easier to read by that metric.
My assembler days were 4 decades ago, but
"Everyone can read Go code and understand what happens."
There seems to be a difference between "easy to read" and "understand what happens" - or what happens on what level. The challenge is that there is a tradeoff between the two. Assembler is too low to understand what "really" happens, on the other hand Haskell for example with Monad stacks is again very easy to read + understand what happens "most of the time", but hard to understand all the abstracted away side effects.
In Haskell with
everything can happen beside what you see.In assembler
nothing happens except these two instructions.The tradeoff is how much you want to be explicit, with the downside of creating too much noise, and how much you want to abstract away, with the downside of magic happening somewhere.
Yes. And to be less snarky: Go doesn't really sit on the efficiency frontier here:
It requires you to write a lot of stuff by hand and is incredibly verbose, but it also does a lot of magic behind the scenes.
Go is "simple" insofar as you're doing simple things. If you've tried writing a KV store or database (as I have) you'll quickly find yourself wanting slightly more modern language features.
"quickly find yourself wanting slightly more modern language features."
Use the tool that works for you.
Feel free to take a look at any more complicated GoLang code (k8s, gorm, etc) and you'll see that the tool/library you're depending on requires a veritable rats nest of bad practices to work around the Go's inherent limitations.
My opinion on Kubernetes as a yardstick example of Go is this:
This was one of the first large systems developed in Go, not too long after the language hit version 1.0 (ca. 12 years ago). What constituted good Go style and package architecture were not well known at that time even. Given that and hindsight being 20:20, I could easily imagine the internal architecture (not even the public surface) for Kubernetes being a lot different and simpler. The package architecture alone makes me cringe. As a hypothetical counterpoint, I wonder what Kubernetes would have looked like had Dave Cheney (https://dave.cheney.net/) and Rob Pike built it in that era. I think that would have been a better yard stick.
I have a unique appreciation for this first large systems perspective as I co-designed an adjacent product in the cloud native ecosystem at that time (Prometheus). There was no good example to follow when it came to large program structure and design in Go at that time. We were all figuring that out for ourselves — organically.
I think this point about organic evolution of the architecture is important to call out explicitly, because developers often look at an existing structure and essentially mimic it with their additions, changes, and refactors irrespective of whether the structure was correct for the problem it was trying to solve or even good. Given that reality, is it really any wonder that one of the first major pieces of software ended up being this metaphorical mess? And the truth is that's not a language-specific problem: it could have happened with any new language. And taking a legacy system of this age and refactoring is difficult from a social perspective, ignoring the gradient/cost of refactoring that is language-specific. You'll have a lot of people who will oppose structural change just because, so probably a significant refactoring to achieve these goals is just not in the cards.
Had there been so much as something similar to https://google.github.io/styleguide/go at that time (ca. 2012) (it's based on the very spartan https://go.dev/wiki/CodeReviewComments), that would have been tremendously useful and impactful in helping preserve simplicity.
Every time I read a critique of Go, I feel the same way: I'm still going to continue to use it anyways. The reason why I am going to continue to use it anyways is because while I understand that it has plenty of easily documented issues in theory (and that people regularly do actually run into in practice), I still find that it is one of the better programming languages in practice anyways. Some of the things that people commonly list as shortcomings I don't agree with (I like the explicit error handling everywhere) and for other things... I often agree, but it doesn't bother me much more than the shortcomings of other programming languages, of which there is certainly no shortage.
I guess I feel bad for people who are particularly sensitive to the areas that Go does not succeed in, because they are probably going to be complaining about it for the rest of their lives. The truth is though, I don't use a bunch of rationale about what is the best programming language to choose which one to work on for a project, I choose a language that I feel works well for me, that I feel I can consistently write good software in, that feels, well, nice to work in. Being a person that values correctness, I do sort of wish that language could be something more like Rust, but for now, it's just not, though it's not like I hate Rust, it's just not what I reach for in a pinch.
Enough has been written about how terrible Go is by this point. At least now I know what it's like to have been a fan of PHP a few years ago! (That's an exaggeration, but it's not that big of one in my opinion.)
> I guess I feel bad for people who are particularly sensitive to the areas that Go does not succeed in, because they are probably going to be complaining about it for the rest of their lives.
Well, that’s a stellar endorsement of the article, because that’s literally the point they’re making.
You’ll use go.
…and then regret it.
…but by then it’ll be too late, and you’re stuck with it.
I think the author makes a compelling argument, which is very difficult to counter, that it is a bad choice and you will regret having it in production in many of the explicitly listed cases, and in many professional situations where companies that are not technically competent use unsuitable tech.
Companies should stick to boring tools.
…but, for personal projects? Sure, go for it.
There's no tool boring enough to prevent any chance of regret. At the end of the day, it's really, really difficult to anticipate where your pain points will actually wind up in the real world. In practice, I've had lots of really good production success with Go and not too much heartache about the choice. Since adopting it personally (in around 2014) almost every company I've gone to work since has used Go in some capacity and that capacity was usually growing because it was working for them.
Will you regret choosing Go? Maybe. Or, maybe not.
>There's no tool boring enough to prevent any chance of regret.
I'm not so sure. I know C programmers that swear by it, warts and all, with absolutely zero regrets for using it e.g. in the embedded space.
If anyone tells you C is "boring", that's just plain and simple bullshit. C gives you undefined behavior, buggy compilers (yes even for simple C code, MSVC is especially bad at plain C but there are other offenders) and the world's worst standard library for manipulating strings. Using C in embedded development is probably OK, even if you have to suffer with whatever crappy vendor compiler you are stuck with, but that's only considering the pretty severe limitations that very resource-constrained embedded development typically has (e.g. no dynamic allocation.) C is only as boring as you force it to be, and you really have to force it to be.
That said... the thing about the embedded space is that most of it is C, always will be, and may continue to be for the foreseeable future. It's really hard to really know what you have to regret if all you've ever known is C.
>If anyone tells you C is "boring", that's just plain and simple bullshit. C gives you undefined behavior, buggy compilers (yes even for simple C code, MSVC is especially bad at plain C but there are other offenders) and the world's worst standard library for manipulating strings.
Boring doesn't mean "has no issues" or "takes care of everything for you".
It means, stable syntax, lots of mature tooling and compilers, and any problems the language has are well known.
Of the various languages around, C is the least likely one to have "buggy compilers" (issues in MSVC because MS compilers focus on C++ are a different thing, also falling in the "known" category).
> It means, stable syntax, lots of mature tooling and compilers, and any problems the language has are well known.
In case of C what it really means is that the compiler codebases are extremely old and sometimes in rather bad shape.
Speaking of stable, C hasn't just stayed still: The C23 standard adds plenty of fun new things, like an annotation for unreachable code. Modern C has threads and synchronization primitives, #embed, complex numbers, and plenty more.
> Of the various languages around, C is the least likely one to have "buggy compilers"
C compilers are still routinely buggy, because the memory model is surprisingly tricky, there are many edge cases in the specification that are subtle and despite being fairly basic are not stressed often enough to come up, and because optimizing C while adhering to its rules about defined behavior is an unending arms race about what is still "technically" compliant with the standard.
Again, this is especially true if we're considering embedded development where the compiler you have might be a random hacked up old build of some vendor compiler rather than at least being recent GCC or Clang. In that case, even if you only consider a small subset of C99, there's still plenty of room for things to go wrong.
By any standard you can come up with, C is just plain-and-simple not a boring reliable solution. As it turns out, something being old and relatively simple isn't enough to make it boring; you can still have a pathological case that is just an absurd clusterfuck.
I will grant you one thing: it is the most boring embedded development solution. But come on. The best modern competition is Rust. Being more boring than Rust is not exactly an impressive accomplishment, especially when you consider how much practical value Rust has to offer.
Go is as boring of a tool as it gets. Which is why I will happily use it
No, because it's an example of someone who chose Go and didn't regret it, and continues to choose Go, because the objections of the article are in practice just not very important or compelling.
There's plenty of software I work on from time to time that's written in Go which I'm happy are written in Go. Some of those were even projects which I started and chose the language for.
Then there's software I've been involved in where I believe Go was the wrong choice. And software not written in Go where I believe Go would have been the wrong choice.
My point is, Go is just another programming language, with a unique set of strengths and weaknesses, and there are plenty of cases which i have experienced myself where there are genuinely no regrets around picking Go. Not because there aren't shortcomings in Go, but because there are plenty of cases where those shortcomings don't matter.
In my professional life, I have seen significantly more regret associated with choosing JavaScript on the back-end than with choosing Go. But then again, there are services in production written in JavaScript where that just never has been an issue and there are no regrets.
Uhh, maybe.
Where is the tradeoff analysis? Yeah, you might regret using Go when some zero value you forget to fill in somewhere pops up later and ruins your pipeline. But are you considering all the issues that didn't pop up because you chose Go?
Java's boilerplate code? Rust and C++'s lifetime analysis and memory management? C's lack of tooling? Python/Typescript's runtime errors? Functional languages' tiny employee pool? How much did trouble did you save by avoiding these?
Go is very boilerplate. It requires at least 3 lines of error checking every 1 line of actual code.
Also it doesn't have packed structs so it's completely incapable of doing low level networking or to handle binary files (you can of course do all the bitwise operations yourself… but is it sensible to use a language that is more error prone than C in 2024?).
Also due to its lack of A LOT of system calls, you will need to use modules written by someone on github, which will happily segfault if you look at them funny.
So now you have hidden memory management! Fun!
Of course if all you do with go is a glorified jq script… sure then it's kinda fine.
I’m not sure I understand the packed structs complaint. I have used Go to read binary data and it’s quite easy. You just need to ensure that all of your struct fields have fixed sizes e.g. int32 or [4]int64 or whatever. Unless I’ve misunderstood what you mean?
I worked on a project with gopacket. It was completely fine.
Try defining a new packet format and ping me.
Thoughts on C#?
When last I tried it, maybe around 2014? I found it a kinder, cleaner Java with better tooling. Visual Studio (not Code) is still the best IDE I've ever used.
Unfortunately it's not popular in the circles I hang around in and the places I've worked. Now that .NET core is the one true runtime I'd welcome an opportunity to try it again; alas, I doubt I'll have such an opportunity (at least not through work).
I remember the upsides but I'm sure there are downsides I'm not aware of. I'd love to read a critique from someone with real world experience.
Not that you asked me but since Go is my goto language, my thought on C# is that it looks pretty cool. C# with hill-climbing thread pool and async seems rather compelling. I really see only two (major, obvious) downsides with C#:
- It has so much. language. design. This is both a strength and a weakness, of course, but C# really does take this to an extreme. I won't bother making a huge list of examples because I think you get the picture.
- Microsoft needs to stop doing things that harm confidence in .NET. Between the silliness of removing hot reloading from the CLI, the situation around debugging outside of Visual Studio products, and drama around the .NET organization... I feel cautious touching the .NET ecosystem. That's saying something considering the company that backs the Go programming language.
(Note: I have not used C# in production, so I can't speak to what it's like in that case. Seems like it's at least fairly "boring" in a good way for a lot of organizations though.)
Is there a specific aspect of language design that you see as problematic? I agree that it can be perceived as "way too many things to keep track of". I think most of it are small, atomic changes designed to reduce boilerplate that can be intuitively understood (like collection literals, additions to pattern matching and null-coalescing operators). You don't have to spend mental effort on these and if there is a scenario where more idiomatic syntax is available - the analyzer has a good chance of catching it and providing an autofix suggestion.
In my personal experience your average go developer that likes go, likes it because he doesn't really know anything else.
As a result most things written in go are, in my own experience, of lower quality compared to things written in other languages.
So using things written in go is usually my last resort, because I expect they won't be high quality before even downloading them.
That is an interesting experience, as I find the opposite true in most of my cases.
And I am a go developer that likes go and use it for 95% of my coding, including MMO servers. I love programming languages and have used and dabbled in many. I still go to go.
And many go tools I use tend to be pretty well written. But maybe this is just a sample size of 1 vs a sample size of 1 issue.
From go programs at the very least I can expect they won't respect normal GNU getopt command line options, and work terribly with signals and going background in the terminal and so on.
If it's a server, of course it won't support any way of communicating it's ready. Not the old fork way and certainly not the new sdnotify things.
why would it have to respect GNU getopt? When did that become the golden standard? I never respect getopt because I really don't care about it and it has no bearing on anything I build. As long as they are documented under `--help`. Almost everyone uses Cobra from command-line options. And it is capable of doing getopt if you want, but I don't see why it would be a requirement.
Signals and Backgrounding seem to be just developers that have little experience with that stuff. I can't remember the last time I did any sort of signal handling to do anything specific. And I haven't had issues with backgrounding, but that might be just the tools I use and the infrequency of backgrounding that I do.
Most servers I interact with or work on have some sort of `health` api endpoint to get that status because most servers in go are HTTP. What more are you expecting? I don't even know what you are referring to by the `old fork way` but I will agree most don't use sdnotify, as that makes the assumption you are running Go on something that handles systemd notifications.
I am fairly certain a majority of Go servers run in a container, specifically an Alpine, distroless, or scratch container. So communication would be over stdout or some kind of API endpoint, be it HTTP, GRPC, or any other TCP/UDP endpoint.
I sort of like Go. The explicit error handling is a little obnoxious sometimes, but it's just a way to get things done. Otherwise I see its simplicity as a strength. It is very ergonomic, easy to pick up, and generally performs pretty well. I perhaps wouldn't pick it for every scenario, but there are plenty of scenarios where it would be a good tool.
Then again, I sort of like Java and Python too, two languages I am proficient enough at. All of those are good tools for what they intend to be.
I don't understand why people get so passionate about programming languages. They are tools. You may like soke more than others, but that doesn't invalidate the ones you don't like.
>I don't understand why people get so passionate about programming languages. They are tools.
Because when you're a professional programmer, tools are a huge part of what you do and how you do it, same like a race driver would need to be passionate about cars.
It's just that for an e.g. carpenter, tools are more or less standadized and simple enough to evaluate.
If saws and hammers and routers had as much variety as programming language tooling, and were as multi-faceted to evalute, carpenters would be absolutely obsessed with using the good ones - even more so than they already are.
> Because when you're a professional programmer, tools are a huge part of what you do and how you do it, same like a race driver would need to be passionate about cars.
I am a professional programmer. Have been one for more than two decades. And perhaps for professionalism, I think there is no space for passion when it comes to choosing the tools of the trade. Passion would make me pick unsuitable tools because well, I would be passionate. Passionate people don't tend to make rational decisions.
I would expect a professional carpenter to be the same. They may have preferences due to familiarity, positive experiences, etc and so forth. But passion?
I think if you tried to tell a professional carpenter that you'd replaced the contents of their toolbox with the equivalent pieces from a discount hardware store you'd be looking for your teeth on the floor.
I certainly wouldn't give up my electronic hardware repair tools without a struggle, it took me years to find ones that I like!
> I choose a language that I feel works well for me
Which is the wisest choice for everyone. Golang is only a problem when a manager imposes it on you.
"Manager imposes it on you" just means you work in a team rather than alone. You can pick whatever you like for side projects, of course you're going to use whatever your team uses otherwise.
"The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt."
Excerpt of the talk "From Parallel to Concurrent" by Rob Pike from Lang.NEXT 2014
https://youtu.be/uwajp0g-bY4?si=EcOXVaGJ1ObFILb6
People tend to forget that Golang was created on purpose for poor programmers.
===
Add: @zwnow
> The industry is still built upon people who can build fast. Go allows that and so does Javascript.
Sorry, I forgot we are on Hacker News, where confusing web development with the whole realm of software development is par for the course.
> People tend to forget that Golang was created on purpose for poor programmers.
Nobody is forgetting that quote. Trust me, it has been repeated a lot[1].
That said, I think this framing of the issue really needs to die. Rob Pike is saying they're "not researchers", that they're "typically fairly young", not that they're poor programmers. Notice that in the list of languages they may have learned, "C or C++" is present. The idea is not that Go is designed for people who can't possibly write C++.
This framing also implies that the language being better for n00bs means that it's also necessarily worse for everyone else. There are some tradeoffs where this is a defensible position, but I think on the whole it's just not generally true. A good example is preferring composition over inheritance: I think the former is generally more understandable and a lot of people actually contort C++ to use it this way too. (For example, in some codebases, only pure abstract base classes are ever inherited; everything else is final.)
When I see this quote repeated as if it implies that Go is just generally designed for bad programmers, I feel like it reads like flamebait. The real answer is that it was designed to be so easy that any idiot can use it. Or in other words, Go is very grug-brained[2].
To each their own, but it's been over 10 years since that quote and Go has evolved a lot. Is it perhaps time to put it to rest and stop reading into it so much?
[1]: https://hn.algolia.com/?dateRange=all&page=0&prefix=false&qu... - though I'm sure it has been paraphrased and linked even more than this.
[2]: https://grugbrain.dev/
> People tend to forget that Golang was created on purpose for poor programmers.
Poor programmers by Google's standards. I would argue the vast majority of programmers, even those outside of Google, don't want to be language researches, have no desire to be a language wonk, but want to build software to solve their and their companies problems. I read that quote and think it means that Golang is the only language the vast majority of programmers should be using unless they are researchers, not as a some veiled put down.
I managed to make go segfault multiple times (a real actual segfault). It's not a general purpose language. If you want to do things that aren't json RPC is awful.
A general purpose language must be one that you cannot cause to segfault? That's a rather... unique perspective.
If it was created on purpose for poor programmers, it seems to have been created to enable poor programmers to write the poor code they wanted to write, instead of making it impossible for poor programmers to write any code.
I guess that's the difference, if you want code, no matter the quality, you have one choice, if you want code that's correct, you have another.
Yeah
I feel Go is simple in the way people say "C is simple" (without the footgun part)
It was created for one purpose and it kinda works but it's clunky. Like there are no fancy front loaders or backhoes and there's a limit on how much out of that form you can get.
You can call us poor programmers if that boosts your ego. The industry is still built upon people who can build fast. Go allows that and so does Javascript. You might not like it but that's what is earning most people their bread nowadays.
> The industry is still built upon people who can build fast.
Correction: the commercial software is, not the industry. The industry and corporations are capitalising on quality open-source software, meticulously written off working hours with a straight head and passion, and a great attention to details. The fact that you can write glue fast enough to satisfy your SLT is predicated on the presence of those quality components you're gluing together for free.
Lot of open source software is written by people who get paid for doing so.
What, no quality tools exist written "with passion, and a great attention to details" in Go? *Doubt.
Commercial software is what pays my rent though. Why would I bother about open-source software other than personal interest?
I don’t think this implies it can’t be the right tool for the job, even when everyone on the team is a good programmer.
I don’t think it implies that you can’t write good programs in it.
> People tend to forget that Golang was created on purpose for poor programmers.
Ergo: if you actually like it, you must not be that great a developer.
Probably, yes. Though that's an empiric finding, and doesn't follow logically from the premises.
> People tend to forget that Golang was created on purpose for poor programmers.
The money shot....
[flagged]
> which casts a doubt about developers actually liking it
I'm self-aware enough that I'm not clever enough for the likes of C++ or Rust; I wish more people were as aware of their own limitations. Managers have a long-term responsibility about a codebase too.
"Clever" developers have no place in professional software development, imo.
More generally, when it's not well suited for the problem to be solved. Eager coworkers anticipating Google level traffic may want to write the system in Go and multiple microservices when a simple FastAPI server would do.
Only a problem when a manager imposes it on you for a program where it doesn't fit well.
Plenty of software engineers don't know any better themselves
I like Go, but after writing/reading so much Go code, I get nightmares from `if err != nil` demons punching me in the face. There were so many nice suggestions made to fix this, but there are some extremely conservative and extremely loud-spoken community members who killed all the proposals with vitriolic feedback. Now, the Go team has given up the battle for improving error handling since they are psychologically afraid of these folks.
Every Go developer survey has results where the community overwhelmingly votes to improve error handling, but a few extremists have derailed all progress.
> Go does not succeed in, because they are probably going to be complaining about it for the rest of their lives.
A lot of people really don't like Go because they have experienced other language features. Go has taken an arrogant stance at trying to make the decision about what features you might need and has given you a very small amount of things to work with.
Counterpoint, other languages - notably Javascript, Scala, PHP, maybe Java - have taken the stance that they adopt other languages' features, not because the language needs it, but because developers were clamoring for it. Which led to added complexity, because they kept adding more and more ways to solve a problem or structure an application, which led to every codebase being so different from the next that the amount of transferable skills and knowledge became less and less. Scala is the worst offender for that IMO.
One frequent praise one hears of Go is that a developer can open any codebase and immediately understand what's happening. Go is readable and stable, which are critical for code for the long term.
The one language feature that I miss in most languages is pattern matching. I wonder if there's any minimalistic language that implements pattern matching well?
I love the fact that I can pick up a Go project from 5+ years ago and it still compiles with the current toolchain. I might need to do a 'go mod init' first.
It didn't get 67 new features that start shooting deprecation warnings on old functions in the meantime. I don't have to learn new paradigms or switch integral parts of my code to something new.
Generics has been in Go for dunno how long, haven't used it once. Didn't need to.
I used generics once, was kinda useful, but definitely avoidable. The only feature I could see myself using is something Linq-esque for slices and maps. Otherwise I’m content.
And a lot of people using Go have experienced other language features as well and either decided against them or that the whole tradeoff was not worth it.
I will keep very fast compilation times and decade long backward compatibility over a lot of your features. Because those are features too.
I mean I miss some language features for sure, but the problem with adding language features is that it adds long-term inconsistency. Take a Go codebase from 10 years ago and it should look mostly the same as it would if it was rewritten in modern Go. Do the same with Java and across generations you'd go from straight for loops, to generic iterators and their for-each syntactic sugar, to for-comprehensions, streams and their functional programming style, to whatever Java is up to in 2024, I stopped paying attention years ago.
>I like the explicit error handling everywhere
Then you're doing yourself a disfavor by using Go. In other languages it would be even more explicit, mandatory, and automatically checked whether it's handled!
Exactly. Every programming language is a tool in your toolbox, and you should choose the appropriate one for the job at hand. For me, that's Go around 95% of the time.
I have no need to worry about a 24 byte overhead for a slice allocation, if I did have to worry about that, I'd probably use C or Rust.
And since Go is so readable, theoretically getting the core functionality out and rewriting it in a more specialized language would be fairly straightforward. And while it's an investment in time and effort to rewrite a chunk, at least you know what you're writing already.
But that's a point made in the article, that Go is also good for prototyping. But there's a few languages good for that, e.g. Ruby which powered a lot of the HN startups in their early days until parts needed to be rewritten for performance / scalability.
But writing a new, unproven product in a high performance, high difficulty language from the get-go is cargo cult; you hope you will need the performance and correctness offered by the language. Meanwhile, halfbaked hack jobs like Facebook and Twitter exploded in popularity and revenue, and while their performance issues gave their developers headaches, at least they knew what problems they had and had to solve instead of guessing and hoping.
> And since Go is so readable, theoretically getting the core functionality out and rewriting it in a more specialized language would be fairly straightforward.
You'll have to rewrite your whole program (or at least factor out the "core" part you care about into a separate binary and talk to it via IPC or network) because Golang has terrible FFI as pointed out by OP.
Every time I work in a different language I'm always wanting to go back to Go even if it's not the perfect language.
I just love the fact that it literally just works. You install Go, you download code, and write code that's it.
No figuring out what version, runtimes, configurations, build tools, package managers to use. Just install and Go.
I think maybe Rust is the only other programming language that provides the same experience.
Maybe these are just lies I'm telling myself, but every time I use Python, Typescript, or Java I dread programming because I just want to write code and I'm often debugging things or figuring out related to configurations, package managers, build tools and versioning.
This is the power of Go, its integrated toolchain. It makes interop more difficult like the article says, but in my personal and limited experience that's not a frequent use case.
> I've started caring about semantics a lot more than syntax, which is why I also haven't looked at Zig, Nim, Odin, etc: I am no longer interested in "a better C".
Well the post rambles a fair bit, IMHO. The whole bit about Go being “accidental” is BS given that Rust is just as much “accidental” in its origin and design.
One thing stuck out to me is that Nim certainly isn’t a “better C”. It has a GC or you can use reference counting. You can use it as a better C if you really want.
Nim’s type system avoids many of the problems that Go has, though it’s not nearly as pedantic as Rust.
At the end of the day lots of software has been written and shipped in Go which runs fast, has minimal downtime, generally seems effective, and has minimal security issues. I’d argue (much) fewer software projects have been shipped in Rust. Firefox is still 95%+ C++.
> I’d argue (much) fewer software projects have been shipped in Rust. Firefox is still 95%+ C++.
It's funny but this comment reminded me of this tweet[0] from 2022 (!). I don't have a horse in this race as I am happily using Python and C++ at $DAYJOB. I'd argue that even if much less software has been written in Rust (source?), it still qualifies as "lots of software has been written and shipped" with it. Not to mention all the investments by $BIGCORPS in the language.
0: https://x.com/m_ou_se/status/1599173341117435905
(2022) Discussions at the time:
(130 points, 148 comments) https://news.ycombinator.com/item?id=34188528
(748 points, 544 comments) https://news.ycombinator.com/item?id=31205072
Not sure why Go is compared to Rust all the time, whilst most appropriate comparison is Java.
I think this is exactly the right way to understand Go - it's targetted at building servers in environments where having strong consistency of code and a short ramp up time for junior engineers is valuable - i.e. it's perfect for all the big corp scenarios that Java was used for.
I think maybe the more common, but less helpful comparison of go vs rust comes from the fact that they are both part of a new wave of languages and that they both default to producing staticly linked binaries.
> consistency of code
There are many stylecheck tools that should be apart of a good stack. Accepting the creator's style is putting a lot of weight on their opinion. Most organizations have their own preferences for good reason.
> short ramp up for junior engineers
Junior engineers aren't a place you're concerned on being productive. Most of the time at that stage in someone's career they should be learning more, getting up to speed with professional practices, tools, and trying to learn code bases+patterns. Ramp up time for a language is a very minor consideration.
Both of those things have very little to do with server environments.
Bigger corporations struggle with Go's module system and forced reliance on seperate repos for separate modules. (Can you bundle multiple modules in the same repo.. yes but you're going to have a bad time)
> Both of those things have very little to do with server environments
My experience of bigcorp is that they need lots of servers (http is the modern bailer twine) and want developers to act as far as possible as indistinguishable resource. They will have rotating, high churn, globally distributed teams of vendors, contractors, consultants, internal staff and the teams will encompass a vast range of skill levels and abilities.
Some languages amplify skill level disparity, some attenuate it.
Go is an iteration of C, not of Java.
It's a really bad choice for situations where Java is a good choice as not only is the language limited, the ecosystem around it is also very limited when compared to say Java.
I'm maintaining Go, C# and TypeScript as my main languages as that gives me excellent coverage. I'll add Rust to the mix when I have 6 months where I can accept the productivity drop or have a project where the quality requirements are lower (it only takes a week or two to pick up a language, it's learning how to engineer within the ecosystem which takes the time).
How is Go an iteration of C? You can't use Go to write a kernel, or program a microcontroller, or for high-frequency trading or a web browser or a tensor library or a language run-time. It's either a bad idea or simply impossible, depending.
Someone please explain to me what's C-like about Go other than vaguely the syntax and that it compiles to machine code.
To be pedantic for a moment...
> You can't use Go to write a kernel ...
Not a production kernel, but MIT did use Go to "study the performance trade-offs of using a high-level language with garbage collection to implement a kernel" [1]
There is also gVisor [2] which implements, as best as I can describe, a kernel in user space. It's intent is to intercept syscalls made in containers and to redirect its execution in a sandbox.
> ... program a microcontroller ...
I'm not sure if one would classify this as a microcontroller, but USB Armory did write a, iirc, Go compliant runtime for bare metal ARM and RISC-V [3].
There is also TinyGo [4] with the following listed microcontroller support [5]
[1] https://github.com/mit-pdos/biscuit
[2] https://gvisor.dev/
[3] https://github.com/usbarmory/tamago
[4] https://tinygo.org/
[5] https://tinygo.org/docs/reference/microcontrollers/
The Go language was actually created while waiting for C++ to compile. Their goal was to create something that was better than C++ for network services and they succeeded- but that's a pretty low bar! Most companies that don't have major performance concerns don't use C++ for networked services. If Rust was already mature it would have been a good option- except for the compile times which was one of their main original issues could well be worse in a Rust code base! I have seen this myself- Go being used instead of Rust with the biggest reason being it compiles so much more quickly.
Because someone decides what language to write a new thing in is very likely to consider Go and Rust. They are very unlikely to consider Java.
Are Rust and Go sufficiently different that they should each be chosen in different cases? Sure! But that’s literally why someone would consider both and pick one.
They should consider Java though.
People have an irrational hate for it based on the enterprise cruft and horrible third party frameworks you can just completely ignore if you build a new thing
It's not good for commandline stuff but for a long running small service it is pretty great
That's a big if. In practice you're not going to be able to escape needing to interface with Java code others have written. What then? You either waste time putting up shims in front of every single API or, more likely, give up and just go with it, at which point you've lost.
It is much more practical to choose a language that does not have a terrible history of poor API design to begin with.
There are plenty of good libraries that don't suck you into a hellscape of frameworks or enterprise java
What language that's been around for long enough to have a large ecosystem doesn't have a terrible history of poor api design?
But sure, if you pick a really new language, you do get the chance to be that history for future generations
^ this! Java's far more capable than golang and a better choice for many projects.. just don't use the decades old "Enterprise" stuff.
^ this! Java's far more capable than golang and a better choice for many projects.. just don't use the decades old "Enterprise" stuff.
..and I'm generally a Java-hater as the language itself is objectively inferior to my beloved C#.. but even then there are situations where Java is a better choice.
> They are very unlikely to consider Java.
Except they are not unlikely to consider Java.
It's a language with a very robust ecosystem, it is relatively easy to hire Java developers, and a decent job for large, complex projects.
One of my biggest beefs with Go and dozens of other languages is: static typing without a sufficiently developed type system is masochism.
If I can't get at least ADTs and parametric typing, give me gradual like CL.
While the article raises some valid critiques, it often overlooks the fundamental tradeoffs that make Go successful in its niche. Go’s simplicity is not a flaw but a deliberate choice, enabling rapid onboarding and maintainable code for large teams. Its explicit error handling may seem verbose but ensures clarity and avoids hidden surprises common in exception-heavy languages. The complaints about ecosystem isolation and tooling ignore the fact that Go provides powerful built-in tools like pprof and dlv, which are better suited for Go’s runtime than general-purpose alternatives. Go isn’t trying to be Rust or Python—it’s a pragmatic tool for scalable, performant systems.
This. And there is one technique that would help in this.
Too often I see code like "xyz := pkg1.SomeFunc(a, b, c)" that makes xyz's type non-evident, especially when interfaces are involved.
Please write Go code like
My 0.02€, YMMV.I don't get it. Why not use a code editor that shows the inferred type on hover?
That should not be an issue, unless the code is written in notepad. As for interfaces, a better approach is to return struct and accept interface in your functions wherever it is possible.
I love Go, so I am biased. However, the beautiful thing about Go is that it doesn't even attempt to prevent classes of bugs by making them impossible. It's a car with ABS but no lane assist, with power steering but no collision detection.
Out of all the bugs which Go permits I have yet to see one which could survive in production for some time without being discovered. Almost all of them would cause a failure the first time a segment of code is run.
Instead it is a plethora of footguns which are attempted to be managed through convention.
This post is always a scary read:
https://www.uber.com/blog/data-race-patterns-in-go/
I’m successfully using Go and agree with the author at the same time. For me it’s not a choice between Go and Rust- Rust is not as productive for high level code because every API maximizes performance.
Since this article was written Go introduced generics which does solve some of the complaints. The rest I mostly solve with linters, libraries, and conventions.
Go has a syntax that is well designed (arguably one of the best) if you’re aiming for familiarity with C style syntax in an imperative language. Other than that it’s a poorly designed language with by far the best engineering work having gone into things that aren’t actually the language per say (cross compilation, asynchronous runtime, GC, module system, super fast builds, etc). They wrote an ACM Queue article essentially stating that: https://cacm.acm.org/research/the-go-programming-language-an...
> Evidently, the Go team didn't want to design a language.
This excerpt should be enough for you to know if you will find the article insightful or useless.
And this is why I normally start by going to the HN comments before reading an article.
It's weird. At a time I was looking for a "better" python. Something simpler and safer than C/C++ but faster than python and more importantly that can produce a single binary. So I looked at everything from Rust to obscure language like Hare.
Go should have been the obvious choice but for a reason I don't understand I dislike its syntax. For Rust I understand: it uses a lot of special characters that aren't easy to remember and to type with a non qwerty keyboards (plus other unrelated pain points). For the different lisp, it was the parenthesis and the reverse polish notation. But for Go, I'm unable to rationalize why I don't like it.
For the anecdote, I settled on compiling my python code with nuitka. No speed gain that I'm aware of but I can now provides a binary. I'm also looking more and more at C# because of its progressing AOT compilation and although I dislike the verbosity of its base mode and the fact it's so tied to windows.
I liked a lot nim and crystal but the small community was a barrier, although I'm really impressed by what nim is managing to do with such a small community and it may me think it's an excellent language
(I will try to motivate myself to pick up one of the language I mentionned above also)
Nuitka is compatible, but comes with the shortcomings of python C-API
There are other approaches which give up on C-API and build a bridge to languages such as Go and Rust.
I've spent a few years working on py2many. Would appreciate feedback on the approach.
>and the fact it's so tied to windows
Dotnet Core is not tied to windows except for certain frameworks like wpf (and there are alternatives for it that work everywhere), credential store.
And it's actually really good to use these days.
Yup, common misconception for folks that haven't used it in a decade.
Our team uses C#. We dev on Apple silicon Macs. Some use Rider, others just use VS Code. We build on Linux via GitHub Actions. Ship to prod running AWS t4g Arm64 instances.
C# to me is like TypeScript++. The language, syntax, and core constructs are close enough that anyone with a good handle on JS and TS can pick it up easily and be productive.
Sorry for the segue, but how your team's experience with C# on VSCode? Any recommendations for plugins? I've heard of a lot of people recommend Rider but not much, aside from neonsunset, talk about VSCode.
Great.
C# DevKit is really all you need (and the same plugins you would normally have like GitLens, etc.).
Refactoring experience isn't as good as Rider (JetBrains are kings of refactoring tooling). But for all other cases VS Code is fast and ergonomic.
Rider does have some nice things for supporting working with SQL databases that I do envy once in a while.
Just in case: DevKit is optional and requires an account, you can just use the base C# extension which is what provides the language server and the debugger, if you prefer VSCodium there's a fork of it which packs Samsung-authored netcoredbg instead of vsdbg so that is covered too.
For F# - Ionide works great, I like it a lot, integrates seamlessly with existing C# projects.
Go is great for a variety of projects. Smaller codebases, little cli tools, simple servers, etc. It has its quirks, as any language does, but it is foolish to write off an entire language when some of the very properties you dislike may actually be a good fit for the problem at hand.
When engineers focus on language features they focus on the wrong things. Yes, languages like Java and Rust are fantastic for large codebases because generics and strong encapsulation become especially important as the number of engineers working on a project scales. Contrarily, Go would probably be a million times better for a small project of one to three staff that primarily revolves around a few well-designed data structures and basic array manipulation. In this context, the more sophisticated features of a Rust or Java are actually hindrances and end up making things more elaborate than needed.
Languages are a resource and a tool, not an identity marker. Use the language that makes sense for your constraints. Programming is all about modeling problems. No language will do this for you and each one has features that help or hinder and some are better or worse form problems of different shapes. Learn about how computers and compilers actually work so that you are empowered to use a wealth of languages and can choose the best one for your project on rational grounds.
Or, you can embrace Haskell and use the only perfect programming language to have been thus far invented :)
Go is just an evolution of C, it works at a higher level but shares many ideas. If you can’t appreciate the simplicity of C, then you probably won’t appreciate the works of Bell Labs, and I have lost all interest in the debate.
I have recently started to port a large codebase from Go to Rust. I thought it was going to take more code in Rust, but I have been surprised at how much less code it takes. As of right now, the Rust version is 60% smaller in terms of LOC.
I think a large part of this is due to error handling in Rust and the sugary `?` syntax which is so incredibly useful.. I can't count how many times I have done the following in go
x, err := Foo()
if err != nil { return nil, err } // or something like this
I'm pretty sure a large chunk of the LOC reduction was in that alone. Though, a large chunk is also some crates that make a lot of boilerplate go away (serde, serde_json, etc).
I remember it took a me a bit of investigation to make Ruby call Go functions, but it wasn't really hard: https://dario.cat/en/posts/portal-between-ruby-and-go/
The argument about FFI and CGO was the most unappealing to me. If you really need to do that, it feels that at some point some decisions were made that weren't the right fit for the project.
Also, I feel that many arguments in the article are potential pitfalls for unintended side effects I've experimented in other languages, like Ruby itself. Keyword arguments are nice, but I know that they've bitten me several times.
Go forces the developer to be explicit about their intentions, that's why I also don't find something bad that your struct functions need to have the pointer receiver. It can be annoying when you forget, but that's what linters are for.
CTRL+F "rust", 24 matches, I had a feeling that would be the case. What does Golang, for the most part, have to do with Rust? I also find the following bit somewhat funny:
>The success of Rust is due in large part to it being easy to adopt piecemeal and playing nice with others.
And as if Rust itself didn't suffer from the same kind of imperfections one can find in Golang. So much for that "nothing new under the sun" back in 2012! But then it starts talking about the "rust shills" boogeyman, and one has to wonder if it's not trying to justify one's choices. (which is fine, anyway) And I agree wholeheartedly to each and every single one of the "lies" listed in the article, that you could very easily rewrite half of which to fit Rust, and the other half requires no changes to apply.
> And as if Rust itself didn't suffer from the same kind of imperfections one can find in Golang.
Which imperfections are these?
His criticisms include:
- No sum types
- Default zero values
- "Go is an island": Fully integrated toolstack makes it easy to use the same tool top-to-bottom, but makes it difficult to use with any other project
None of those are lies; apart from the first, they all have advantages and disadvantages. Sure, there's an advantage to forcing you to specify every value, but there are disadvantages too. And sure, there are disadvantages from Go being an "island", but he lists some of the advantages right there in his post.
If those are deal-breakers for him, then sure, go use something else. But lots of us like the advantages more than we dislike the disadvantages.
The title lacks a (2022) Otherwise great article :)
I liked the idea of a language with minimal syntax that is easy to learn and easy to understand because the code is forced to be straightforward. It didn't work out that way to me in practice.
It's a bit too low level for many use cases in my opinion, and that does get in the way somewhat. It also works against the "easy to learn" part unless you start with developers familiar with low level programming.
I also found some types of library code surprisingly difficult to read, especially when empty interfaces where used.
The standard library was great though, it covered stuff that others don't and it just worked out of the box.
Currently onboarding to go. For me personally, it's too opinionated e.g. for: error handling, mutability, unit testing, naming conventions, lack of features like sum types.
As a new joiner, some things have a "religious" feeling, like: the go gods have given us THE language and we must use it their way, without questioning too much.
I have the feeling other languages are less opinionated and this allows for more ergonomics and comfort when developing.
I suspect it's designed that way intentionally.
It's a language built to make it hard for new joiners to mindlessly add complexity, as the language itself will fight you. It makes it hard to add dependencies that aren't very visible and vendored in, it makes it hard to change how the language behaves with overloading, preprocessor magic or macros, and so on.
It's built to fit Google specifically, with their army of new grads, and deep investment into reinventing all the dependencies themselves, and having a huge monorepo.
If you're not google, you're gonna have a bad time, which is also good for google.
The article is arguing that Rust is a better choice which is laughable. Go is a small GC-ed language. Rust is an obnoxiously complex language with strong emphasis on memory management. I don’t see any projects where you could legitimately hesitate between the two.
Truth be told I quite like Go. It’s small and simple. It does the job. You can onboard people on it quickly and in practice with the toolchain you don’t run into the issue mentioned.
Would I rather use Ocaml most of time? Of course but it doesn’t have the same ecosystem. Would I rather use Rust? Heck no, I’m not going to deal with the borrow checker and life cycles if a GC is fine.
>After spending years doing those FFI dances in both directions, I've reached the conclusion that the only good boundary with Go is a network boundary.
It works perfectly well with stdin/stdout as well, as seen in many LSPs
> I've started caring about semantics a lot more than syntax, which is why I also haven't looked at Zig, Nim, Odin, etc: I am no longer interested in "a better C".
A strange take. Zig, Nim and Odin are about fixing semantics not to bring syntax sugar.
> Because they're Go experts, they know the cost of using Go upfront, and they're equipped to make the decision whether or not it's worth it
Well maybe not.
If I’m an expert in only Java 6, I might not be aware of all sorts of useful features that other languages have, such as sum types, traits, type inference… I only know one side of the trade off.
I might be vaguely aware of those ideas and dismiss them because I can’t imagine how they would fit in my Java 6 workflow.
And yet when some of them arrive in later Javas, I begrudgingly use them. Years later I can’t imagine how I ever lived without them!
We’ve seen this play out so many times.
Remember when lambdas were just for weird functional languages?
Should be called "Lies we tell ourselves to keep Going"
Seems like Zig is actually meeting much of the writer's expectations for good language design. Although I'm still not wholly convinced by it's move from LLVM and hell-bent desire for incremental compilation.
I see this false promise all the time: Simple tools are simple to use.
NO. Not necessarily. Use a handsaw to fell a forest if you like, or a hoe to till 1,000 acres. But I will not. Tool simplicity will come back to bite you if the task you're trying to accomplish is complex or high-scale. And simplicity of design in no way correlates to simplicity of outcome, or of your workflow using the tool.
Golang has, to be fair, more going for it than just its relative simplicity. But in many ways the OG criticism rings solidly true. The "we'll do just enough language features and tooling" becomes an abdication and an excuse in a complex world.
The worst thing - default values as a solution for absent values. What can go wrong with implicit value assignments?!
Would you rather have undefined initial values?
I want to build things and could care less about intellectual masturbation. It was built for proletarians like myself. I like Go because it's neither Rust nor Python. But I do understand that there are times when having some nice abstractions over common patterns doesn't hurt. At the same time, Go has come a long way over the years.
This again? It's reassuring to see the moderation on HN hasn't improved one bit, still as selectively enforced as ever and blind to even the most thinly veiled trolling.
Since apparently this needs explaining, the post title implies everyone who uses Go is delusional, unable to think for themselves, an idiot. The article of course follows suit. How such a strongly emotionally charged article is supposed to lead to intellectually curious conversation [1] eludes me.
Dang's comment in the original 2022 thread [2] is downright hilarious to read. "These flamewars around emotionally charged articles keep happening and we're powerless to stop them!"
Enforce the rules around flamebait fairly, regardless of who the author is and you won't have a problem.
[1]: https://news.ycombinator.com/newsguidelines.html
[2]: https://news.ycombinator.com/item?id=31207191
> and most importantly, you adopted a language that happened by accident.
Is the author implying that Go was created by accident?
"Accident" is hyperbole, but IIRC Go evolved out of Google's internal high-performance HTTP server tooling, so they didn't initially set out to create a general-purpose programming language.
My biggest problem with Go is readability of some projects. In many cases, the code is split across files in an unintuitive way, and you can only navigate with the help of an IDE.
One thing what i observed is any language you right ask AI to optimize in Rust..why its so much of a debate when scale comes. In 6months AI will make any rewrite possible with full context aware. So don't spend your energy on things that can be done by machines and focus on things which u can get deep on for distributing the software.
Okay so I’m wondering, if you’re not in the Golang universe yet, what language is better to start with learning?
I'm curious as well. Are there alternatives to Go in some fields completely dominated by it? (like Kubernetes controllers/operators)
Depends on what you want to do.
Go's channels are probably awesome, if you need parallel computing.
If you want to build in k8s land, you can't avoid Go.
But besides that?
Maybe, use Gleam or Elixir.
I find it fascinating the extent to which language choice makes a programmer emotional looking around at these comments.
Well, the post title implies people using Go are delusional, so that's expected. Starting a conversation by calling someone stupid isn't likely to end up productive.
So what’s the alternative then to go?
When the article started the part "but Tailscale still uses it" I almost felt watched
There's a lot of valid critique of Go, but I've never found anything like it that lets me build lasting, high quality, bug free software.
Explicit error handling means that I actually think about and handle errors. No surprises in production. No random exceptions throwing deep inside some dependency. I've been running some Go services for years with no crashes.
The brain-dead simplicity means I am not tempted to waste time being clever.
The tooling means my code is trivial to build, even years later on a new machine.
I wish I could take the Rust pill like everyone else and be happy.
I am not a stranger to the programming world. I am fluent in Python, Java, Scala, Erlang. I can write acceptable Haskell and C++ and Common Lisp. I am well versed in functional programming (though I don’t hate OOP — it is still a good thing when done correctly, and I am a big fan of Smalltalk).
However, for some reason, I am completely unproductive in Rust. It always fights me. There’s always something I want to do (graph structures? spliced lists?), and it’s always “you don’t need that” and “it’s not the Rust way”. There’s always explicit lifetimes and you miss something and you need to rewrite huge swaths of your code. And nothing is refactorable. It’s not even a good functional language, there are no persistent data structures, and it’s a pain to make them, and no ways to express yourself functionally like I used to do with Scala.
But apparently, everyone and their dog like Rust and are insanely productive in it. What am I missing?
I do miss several of the things you list, too, especially (safe) self-referential data structures. But Rust is a fantastic compromise solution if your number one priority is to match the performance (or rather, performance possibilities) of C and C++, and have memory safety while doing it. If your number one goal is to be able to write memory safe ideomatic code that is entirely comparable to those languages, and then add some functional programming and lots of quality of life stuff to that, then it's pretty sweet.
In other words: Don't think of it as rivalling the entry cost of Python or Java, or the expressiveness of Haskell or Scala. Try to think of it as a replacement for C++, with memory safety and piles of quality of life improvements. And then with a sprinkle of the other things on top.
I am fine with C++ memory safety if I am mostly working with RAII and explicit move semantics when needed (it is not perfect, but it will get us through the night). However, fighting the borrow checker drastically _decreases_ my quality of life, and I couldn’t find the way to get myself to free flowing code as I had in other languages.
Interestingly, I have the same feeling when trying to write something with Vulkan — the amount of ceremony and boilerplate before you even get to the first triangle is a huge barrier for my productivity. I want something more immediate.
I just want to say that I started working with Go a few months ago at my new work, and I've been disappointed. The type system doesn't even have union types, and the implicit implementation of interfaces has caused a couple headaches that I would've never had with explicit languages such as Typescript (strict).
At this point, I prefer strict Typescript with very restrictive ESLint rules than Go. I don't understand how this language was pushed for web servers, it lacks common constructions to represent very frequent data types in API design.
Also, the whole go func(){} and channels is nice if you are coming from a language with worse ergonomics (C?), but when doing multiple web requests to other places and wanting them to happen in parallel, it gets messy fast. It lacks a pretty abstraction -- I very much miss an await-like construction which uses wait groups under the hood.
So, I very much agree with the post. I like errors as values, though.
The superpower of go is goroutines and channels. The kryptonite of go is its limited libraries. Go is a great choice for many concurrent applications. I couldn’t finish reading the article because it lacked focus.
There _are_ two problems with Golang that I _would_ like to wave a magic wand and fix if that was a power I had.
1) Sum types (E.G. C/C++ union types) - Yeah, something similar to that should exist... it's syntax sugar over #2
2) 'casting' / reshaping perspective on data ; as long as something has the same struct size a programmer should be able to tell the compiler 'but this is actually that'. Which also implies a way of fixing the layout of a given data structure. I figure that's why Golang doesn't allow this already.
Yeah, 24 bytes (len, cap, pointer) per slice (dynamic array) has a cost, but if that really gets your goat use a fixed size array, or pointer/reference to such [n]thing.
3) Seriously, for a slice, make it like a map, a pointer/reference to a len,cap,blob - so that when I pass a slice by value I pass the WHOLE slice, not a COPY of len and cap and a reference to the slab. Current golang has the worst of all worlds with passing slices around, in that changes mutate, UNTIL it's resized or manually copied. The current design has the behavior it does to support slices of slices, which requires the pointer to the blob. A more complex scheme of a container behind the slice, and references to that could also work, but would be an entirely different datatype.
I started learning Go and when I got to the chapter on slices is when I dropped it.
I can't put my finger on why it was so off-putting, but it just left a bad taste.
Well, i feel like this is the 100th article i read about why golang is bad "mkay".
For my personal background, i started with golang about 6 years ago and im using it mainly for private and open source projects.
Yes golang for sure isn't perfect, but what language is tho? I think the major point is - the language you use should match your use case. If it doesn't it will always feel "bad" or you will more likely tend to find points why the language isn't perfect (for your needs).
Sure you could write a website builder in ASM or you can write an Operation System in Javascript - but why should you?
Just look at your use case - check your needs and decide what language fits the best.
If its golang? Fine use it. If its not golang, than don't. But don't take a language and try to match it on "everything" and than complain that it doesn't do the job - because no language will.
Thats my 5 cent's on this topic....
Even async in Go isn't that good, ultimately. You can't monitor channels, you can't properly react to errors, you can't shutdown and restart them. A panic in a channel will kill your program. Etc.
It's "we learned about green threads and didn't bother to learn anything else" approach (also prevalent in many other languages)
It's more that it's CSP from early to mid 1990s, over which Go just added syntax sugar
Erm..
> As it happens, I am not a junior developer, far from it. Some way or another, over the past 12 years,
I'd say that's isn't too far from it to be fair.
> I've mentioned "leaving struct fields uninitialized". This happens easily when you make a code change from something like this:
Really? Are you not using gopls? It absolutely will warn you about this. And the mutex case. And a lot of the other similar criticisms.
> Go not letting you do operator overloading, harkening back to the Java days where a == b isn't the same as a.equals(b)
In languages that have it I've never used operator overloading to produce anything good or that was obviously better than just using methods.
> The Go toolchain does not use the assembly language everyone else knows about.
Honestly I've never noticed or had to care. And you can build plenty of things that don't require CGO.
The whole "gnostic" tone in language articles on HN is becoming overbearing. There are parts of the language to criticize but couching it in this "lies _we_ tell ourselves" and somewhat kitchy writing that presents minor matters of taste as fully existential issues makes this an unsatisfying read.
> In languages that have it I've never used operator overloading to produce anything good or that was obviously better than just using methods.
I used a Scala library for S3 once that overloaded + to mean upload.
Which is obviously bad and unnecessary.It's really a feature that should be used very rarely by those who make basic libraries, but there it can make a lot of sense - data structures, numeric types, etc.
That says more about the library design more than it does about the library.
There are much better examples of the operator overloading for example cons:
item :: item2
Later you can break that down in pattern matching.
Oh well, here we go again... (2022)
The article is great. The bigger picture, of course, is that it’s always “pick your poison”.
On one hand, say, I love operator overloading, I love how Python does it (once you satisfy an interface, its operators Just Work).
On the other hand, I can appreciate the choice not to do it at all because half of the ecosystem will do it, and another half won’t. Also, it would require implementing function overloading, and it is a can of worms.
Or generics and rich type systems, which all come with their own tradeoffs. I hear that Rust cajoles you into tinkering with the type system, and wach tweak requires refactoring of more codebase than anyone would like (don’t take my word for it, it’s just what I heard from a few different sources). I know that Nim is so expressive that it can be annoyingly trivial to be too clever and run into a valid edge case that will make the compiler barf and die. Go sidesteps the issue by not wading into that territory, and that may be perfectly okay, albeit verbose.
It’s always picking your poison, so I guess check your tolerances and allergies so it doesn’t kill you before you get the job done…
> I love operator overloading, I love how Python does it (once you satisfy an interface, its operators Just Work).
Is this different from other styles of operator overloading? Why does it matter whether, when I want to overload the + sign, I need to define a function called `__add__` or `operator+`?
For me it's easier to introspect, fewer unexpected corners. Contrast this with operator overloads which you can put literally anywhere.
This is a silly article, though I can only speak for myself. First of all, language design is pretty much the last reason anyone should consider when choosing a language. For me as a solo developer who needs to get results in time, the main criteria for choosing Go were compilation speed, development speed, general tooling, and third-party support. Especially the latter is extremely important; I cannot develop all kinds of libraries like e.g. an Excel or Word document parser in-house, I have to use whatever MIT-licensed libraries that are there.
I've developed in all kinds of languages in the past: Pearl, Java, C++, REALBasic, PowerMOPS (a Forth derivate in the 90s), Ada, Rust, Python, Java, Racket and various scheme dialects, CommonLisp, ObjectPascal/Lazarus, just to name a few. Out of these, REALBasic was by far the one in which I was most productive. Alas, it became unaffordable and the vendor lock-in really burned me. No more commercial languages or IDEs it is for me.
If Ada had a garbage collector, I would probably use it even though the development time is a bit longer. Likewise, I'd love to use CommonLisp and think it's the best dynamic language. But it simply doesn't have enough 3rd-party support, and I'd also be wary about how it runs on mobile platforms.
I've got to say I'm pretty happy with Go so far. Is it the ideal language for me? No, because full OOP with multiple inheritance would make my life much easier. But it works, is easy and fast to develop in, and the results speak for themselves. I have no problems with bugs. The explicit error handling may be a bit cumbersome but it forces you to think about and deal with every error, unlike people in languages with exception system who often ignore errors until so far up the call chain that they don't even know what actually happened any longer. I don't see the point of that. If you have an illegal state of an object it doesn't matter if you call the object nil or DefunctInstanceOfBla, you're going to have to deal explicitly with the illegal state.
Notably, C# was also in my final selection of languages. For my principal use case - distributed client/server applications with GUI - Go's GUI options were not so stellar and I was thinking about using C# instead. AFAIK, C# is very suitable and a great language, too. I decided against it because of the C#'s bizarrely complex runtime library situation and naming schemes (WTF?) and simply because I would have had to learn it first and already knew Go fairly well.
Beware the language aficionados and purists. I used to be one of them, too, advocating Scheme & CL. However, in the end purely practical considerations are always more important. Programming languages are tools for getting things done.
The author's last post they referenced is a bit bizarre. I don't think that some overly simplified and error prone std library APIs is a particularly compelling reason to dislike a language. I didn't read the entire thing though because it was extremely long
“An idiot admires complexity, a genius admires simplicity, a physicist tries to make it simple, for an idiot anything the more complicated it is the more he will admire it, if you make something so clusterfucked he can't understand it he's gonna think you're a god cause you made it so complicated nobody can understand it. That's how they write journals in Academics, they try to make it so complicated people think you're a genius” ― Terry Davis
Please note, no offense intended, I just like this quote to describe Go success.
Context for those unaware: This article was a response to a 2022 HN thread [1] about the original article of the author's. Then the following discussion happened: [2].
First off, let's appreciate the fact the author has managed to write this humongous article in a day. That's some writing skill.
And indeed, leveraging writing skills to unleash a flood of words in order to overwhelm anyone trying to respond is just the first tactic of many used by the author to sound more convincing, which is going to be the point of my post. I'm going to analyze each rhetorical device, fallacy, and bias present in the article.
> The author is a platypus
The point of this paragraph is not to present any arguments, it is devoid of them, but to elicit sympathy from the reader for the poor author who is clearly getting ganged up upon. Comments in the referenced HN thread did not in fact attack the credentials of the author in any way. See Appeal to pity [3].
> Mom smokes, so it's probably okay
Lots of words here, but the gist of it is:
> Or you can be horrified, as you realize that those complex problems only exist because Go is being used. Those complex problems would not exist in other languages, not even in C, which I can definitely not be accused of shilling for (and would not recommend as a Go replacement).
Setting aside the validity of the technical arguments, the tactic used here is to imply that since certain other languages don't have the specific mentioned problems, they are strictly superior. In reality, had another language been used, the Tailscale team would simply be dealing with different problems. To riff off the C example, if Tailscale used C, they'd be dealing with memory safety issues instead.
> The good parts
A clever attempt at a lie of omission. First we list a tiny subset of the pros of Go, then we claim that this tiny subset is not enough to make up for the numerous cons. See Cherry picking [4] and Straw man [5].
> And since, just like C and Java, you do not get to decide what is mutable and what is immutable (the const keyword in C is essentially advisory, kinda), passing a reference to something (to avoid a costly copy, for example) is fraught with risk, like it getting mutated from under you, or it being held somewhere forever, preventing it from being freed (a lesser, but very real, problem).
Here we see a thinly veiled reference to the main bias behind the article. Can you guess what language the author is implicitly talking about here?
> Go is an island
Nothing really interesting here, but it's worth noting that, just like the majority of arguments presented so far, this largely applies to most languages out there (but of course not to the language we're really talking about here).
> All or nothing (so let's do nothing)
The thrust is another strawman, quite simply nobody argues this: "It's not like you can prevent all problems anyway.".
This section is only a lead-up to the next one anyway...
> "Rust is perfect and you're all idiots"
Huh, why do we suddenly have a tirade about Rust in what's supposedly an article on Go? If you correctly guessed earlier that this entire article is actually about Rust, congratulations.
> Folks who develop an allergic reaction to "big balls of mutable state without sum types" tend to gravitate towards languages that gives them control over mutability, lifetimes, and lets them build abstractions. That those languages happen to often be Go and Rust is immaterial.
Wait a second, language*s* with sum types, control over mutability, and lifetimes? So, Rust, Rust, and Rust?
> Because function signatures don't tell you much of anything (does this mutate data? does it hold onto it? [...]
Only Rust is good enough, we get it already.
---
Overall, we have quite a few strong technical points here that could have made for a compelling article, but they're marred by copious use of fallacious arguments and blind fanboyism for a certain language.
---
[1]: https://news.ycombinator.com/item?id=31191700
[2]: https://news.ycombinator.com/item?id=31205072
[3]: https://en.wikipedia.org/wiki/Appeal_to_pity
[4]: https://en.wikipedia.org/wiki/Cherry_picking
[5]: https://en.wikipedia.org/wiki/Straw_man
[dead]
[dead]
[flagged]
[meta] Does anyone else color schema? I found the blue links hard to read with the dark background.
Or maybe the links are left to the default for the browser to decide.
Either way I would prefer an easier to read color.
Golang it's a huge career mistake. You can comeback in 5 years to this message to confirm the obvious.
The author clearly does not weigh in on the "whys" some things work in Go like they do.
So, adding an extra struct field results in the base value, and by design this should be used as a base default. The go proverb make base values useful goes hand in hand.
Obviously functions are not the same. And calling function with the wrong args is a compile time error. There is also CI check that check for this use case.
Overall this seems like a comparison to Rust, and its obvious they are not same same. Apples to Oranges.
Rust is also not the same as C, and therefore shall not be compared to it. Crabs to Cats.
Of course they are not same; that's why we want to compare them in the first place: were they the same, there'd be no need to compare them at all. Since, you know, they would be same and have no differences.
I really wanted to like go, and I tried to write a discord bot using it, but the very opinionated brace style (which isn't the one I prefer to use), and the fact that I struggled to much to try and split my code across two files kinda turned me off it. In the end I just went back to python
Python has the most opinionated brace style though
You can abuse list comprehensions to enclose your code in [ ] instead of having to deal with all that whitespace.
oh that's evil :)
Genuinely curious, why am I being downvoted? Did I say something wrong?
AI can rewrite any dream code which u write and putting ur brain on that is pure seless way of pointing start with some island as you pointed get to scale and in 6months every code can be rewritten how many ever time u want..tokens will be so affordable to run full context code base shift..don't worry about it just focus on distribution for ur product with the code you right..thats a good thing to worry about.