this is too real and my number 1 gripe with go
It doesn’t take a day of use to be frustrated by go’s warts - like having nullable pointers everywhere, the lack of sum types and the awkward error handling. I think it takes more than a day to get used to those problems and work around them. And who knows if the juice is worth the squeeze?
Rust is the same. It takes less than a day to start fighting the borrow checker. Whether the language is worth the discomforts it brings is a question nobody can answer for you.
I think dismissing these criticisms by saying the poster just didn’t understand the language is overly dismissive. These criticisms are real and legitimate.
Wait, Go doesn't have sum types? How do you create any modern programming language and not build it on top of sum types? That seems so odd.
I think it’s admirable to try other languages for a day. I’ve spent less than a day noodling with dozens of languages that seem interesting. I’ve never tried dart, kotlin, elm or Scala. Why not? Is it prohibitively expensive? No. I just haven’t taken the time and the initiative.
More people should spend a day with go, even if they don’t stay around it’s still nice to learn something new!
I've run into this problem repeatedly on Goodreads where wholly mediocre works get high ratings because of aggressive selection bias. I generally like fantasy, but I've stumbled on some painful stinkers thanks to this dynamic.
It's doubly reasonable to value first opinions on languages like Go since they intentionally aren't doing anything fundamentally novel. With a handful of exceptions, Go is a remix of ideas from mainstream languages that we've all seen before. It is about as far from a new paradigm for programming as you can get, and that's the point! Hell, that's the main thing people praise about the language: "I could pick Go up immediately, without needing to learn anything new"...
People tend to always exaggerate how little time it takes to learn a language to a meaningful degree. Sure, the first day I tried Go I was able to accomplish something useful, but it took me a few months to develop a basic understanding of how you use Go in an idiomatic manner. I'd say it took at least a couple of years before I could say I "knew" Go.
And it wasn't exactly love at first sight. My initial impression of Go was that it was a bit too much like C, and, coming from Java, I was a bit confused about how you structure things. That takes time to figure out for any language. What sold me on Go eventually was that for my uses (writing multi-protocol server applications that run on multiple architectures), it resulted in code that was very readable and productivity more than doubled because Go is a lot less fussy to work in than Java. Both in terms of encouraging more minimal designs and having a lot less fragility.
So while at first you think "eww, err != nil everywhere", at least in a decade of reading your own or someone elses' code you'll know exactly what's going on.
How many languages are similar? I feel like a lot of languages in the past decade have had a steady migration in things like error handling, so 10 year old Java code is incomparable to today's Java. Whether that's a good thing (the language has evolved and is now more ergonomic) or a bad thing (I no longer know what's going on here) is the big debate.
Learning a language only starts with knowing the formal spec. Then you have to learn how to express yourself efficiently and in mechanical sympathy with the language. That takes time. Worse yet, a significant portion of developers can't do it without a lot of help.
But yeah not being able to tag pointers as 'can't be null' and have the compiler enforce that ignores everything we've learned in the last 40 years. You shouldn't be able to pass a potentially null pointer to a routine that can't deal with it. You end up with code finding a null pointer without any context that tells it what to do with that. Or it panics.
A basic option and result type could fix a lot of the issues around errors and null pointers. I know languages like Scala, Haskell, and Rust have type systems that are often considered too complex but Go doesn't need all that to add these two.
I have seen so many bugs caused by default values, production outages. Plus the code is harder to understand because you need to consider the default value case, and make sure that it is only left at the default when intended, not by accident. (And the linter probably can't warn you because it isn't wrong to have the default value live in one path through the code).
One of my favourite things in more strongly typed languages is just adding a field to a struct then having the compiler point out everywhere that I need to make changes. The strictness both prevents bugs and saves time. Plus the code doesn't need to worry about invalid structures, you can often write code such that they literally can't exist.
The other option isn't null, the other option is an error. Ideally the code fails to compile. 99% of the time I would rather have an error than have my code silently be wrong. Failure is almost always better than corruption. (Don't get me started on Go's fmt lib that barfs crap into the output if you have your format string wrong, at least that is easy to lint for most of the time) And build failures are almost always the best because then the wrong code can't even make it to production.
Having a (val, err) tuple IS more easy than returning a monad.
Sometimes (most times?) pure procedural code is just the best, and i really hate languages that have feature X but does not support it fully.
As an example i would not be happy with an monadic return type if the language did not have the option to use some sort of bind/return combo, and have full pattern matching support.
Thats why i champion Go for larger teams. Almost anyone can join the team in dive in without much previous Go knowledge. This is a rare feature and i dont know many other languages that has this property.
> Having a (val, err) tuple IS more easy than returning a monad.
Arguments about which concept is easier for people to understand pretty quickly descend into subjectivity but I don't believe this is universally true. Why am I getting a value when there's an error? What do I return as my value when returning errors? Explaining to someone that a function returns (val, err) and only one of these values will be significant is more or less the same as describing a result.
Option is even simpler. It's a list that can have at most one item. Or, you know how in python you can set a variable to None? Here's how to do it in a strongly typed language.
But it also retains a certain "we don't need a robust type system; weak-ish static typing is good enough" ethos that made sense in back then, when compilers were hard enough to write that it was easier to justify making the programmer handle more things manually for the sake of simplifying the compiler authors' job.
This is always a tradeoff, of course, but I think that the optimum balance has shifted even further in favor of programmers. Some of Go's decisions still made sense in the 2000s when the language was first being created. But now, 15 or so years later, I think many of us could be forgiven for wishing for a language that's a lot like Go except that it dared to dream just a little bit bigger.
I care a lot less about it for "simple" errors like typing errors, typos, or whatnot. This is mostly trivial stuff compared to "is this entire approach good or not?"
Rust & Haskell is a big no :p
I've been writing a fair bit of C lately. I'd say that the way Go error handling works has exactly nothing in common with C. Despite spending 15 years mostly writing C on UNIX 20 years ago, when returning to C it was striking how little Go's convention of returning multiple values, with the error being the last, has in common with C.
And if by structured error handling you mean things like exceptions: I don't really see how that improves error handling from a readability or security point of view. In Java people can't even agree on whether or not to use checked exceptions (ie neuter any perceived advantage of exceptions), and syntactically, exceptions are a little bit more awkward than return values as you invariably create nested scopes and move the handling of exceptions away from the normal program flow - which is not a readability win.
But I'll grant you that the error type in Go was both a bit vague and there was a lack of sensible stuff to, for instance, wrap and accumulate errors initially. But that has gotten better.
Structured error handling is as opposed to exceptions. I think that the most famous implementation these days is Rust, but if you want to see a version that isn't built on top of an ML-flavored type system, check out Zig's implementation of the concept: https://ziglang.org/documentation/master/#Errors
I'm not opposed to language support for pushing developers to handle errors. But experience suggests it isn't the magic bullet we sometimes tend to think it is.
I'm only responding to point out that I never claimed or even implied that there's such a thing as an error handling magic bullet, just that I think that Go's approach to error handling has been oversold. It compares favorably with an "enterprisey" approach to exceptions, precisely because that method makes it so easy to hide the dead bodies. But I don't love that Go's (and C's) approach makes it so easy to implicitly suppress errors, because that's effectively the default behavior. In a language that forces you to check for errors, by contrast, it's harder to be lazy and sloppy without being explicitly lazy and sloppy, which then means that such behavior is more likely (not guaranteed - again, I'm not trying to claim a panacea - just more likely) to be noticed during code review.
This is the killer for me. After using Swift for a while, I no longer have the tolerance for languages that don’t have proper option types (and perhaps more importantly, which don’t use option types pervasively and by-default.) It’s just 100% the wrong way to design a language nowadays. Option types are table stakes.
It reminded me of Go vs. Algol-69 http://cowlark.com/2009-11-15-go/
I think it was a good choice to take smaller steps and try to not be too ambitious too soon. Sure, Go isn't the most sexy language from an academic point of view, but it has a certain conservative and pragmatic approach that does work. It does generally result in code that is a lot easier to read and maintain than is my experience with C, C++, Java, C# and a few other languages I've worked in.
Go is an engineering language - not an academic exercise.
Take, for instance, the approach to generics. They could have designed that in from the beginning, but they showed restraint and didn't. That probably took a fair bit of courage. It is my impression that they hadn't figured out what generics should look like in Go, so they postponed until they had a better feel for how it ought to be done. Rather than risk making choices that would be hard (impossible?) to rectify later.
When you add something to a language there is always the risk that you make it worse.
(I'm not making any qualitative judgements on Go generics since, frankly, I don't feel qualified. I make very sparing use of it because it really isn't that often I actually need to make use of it)
People tend to forget that Java didn't have generics until 1.5 (or 5.0 or however they prefer to version it now) - about 9 years after first being launched. And to be frank, that was not a fun experience at all. Not so much because there was something wrong with the design, but because suddenly a lot of people went overboard and started designing really hairy types that could be hard to figure out and use.
If you consider C++: C++ spent 20+ years flailing wildly and the result was that you got lots of different C++ "traditions", subsets and practices. Sometimes within the same company. Depending on which era or tradition a C++ codebase is from you may have to adjust to a wholly different way of programming from what you are used to or prefer. And as for generic programming: in what world is STL a neat solution? And to this day, compilers are slow, they produce rubbish error messages, the toolchain still feels like a 1970s ad-hoc mess, and there is no definitive way to build things, resulting in lots and lots of additional complexity when trying to tame the horrific tool chain.
Sure, they could have put loads of stuff in Go from the beginning. But I think they would have gotten a lot more wrong if they had. I really appreciate that they are evolving the language slowly and conservatively.
Great summary. I personally think Scala is the antithesis to Go, it being an academic exercise - with complexity and tooling to boot.
re: Java generics, I've got this comment from 2015 bookmarked, it's a great explainer about the decision process behind adding generics to Go and the problem(s) with Java's implementation: https://news.ycombinator.com/item?id=9622417
> those ideas simply haven’t had time to pass through the filter of
> practical experienceLanguages like C# (which I've used extensively) and Eiffel (haven't used in anger), on the other hand, have a consistent type system, and didn't seem to have the same problems with generics that Java and C++ did. Or for a non-object-oriented model that has some other features that might be attractive to Go, such as not allowing implicit specialization, there's Ada.
Does https://pkg.go.dev/slices#DeleteFunc not work for you or do you need it to be called filter? It's there for maps too https://pkg.go.dev/maps#DeleteFunc
I'm glad that they implemented some basic library functions 10 years down the line.
You can write a simple Option type yourself using generics [1]. It's not strictly idiomatic and you do have to remember to use it, but it works well.
[1]: (my post) https://news.ycombinator.com/item?id=38331565
Go doesn't have some of the most library functions, so large codebases shared between teams end up with a dozen different implementations of functions like "minimum" or "filter". Good luck debugging a bug in one of the implementations.
The exception-less error handling would be great if they used sum types instead of (val, error) tuples. Return types are required to have a "default" value if you want to return an error, and good luck finding bugs where you use that value and forget the "if err != nil"`.
Worst of all, they removed most "fun" C things about pointers (like subtracting pointers in the same array) but kept the null pointers themselves. There's no way to ask for a not-null pointer at the type level, so you have to check for nullity everywhere and good luck debugging those runtime panics.