While the proposed slightly reduced the token count, saving the average typist approximately 1 second of time if they don't have an autocomplete editor that types it for them, it doesn't change anything about the mental model where the real time is actually spent, so is it really sane?
Worse, it is dependent on the error type, but errors are not always of the error type. Not even Go's own standard library consistently returns errors using the error type, never mind all the other crazy things you find in the wild.
That doesn't sound sane at all.
It is true that Go not having any real kind of superpositions or side channels makes stealing popular ideas from other languages impossible (it already has both well-known error handling methods that do not depend on those properties). But a sane proposal would be designed with that in mind.
Of course if you want something less specific to errors, we already have do-notation as an excellent example to look at. And you can't say that's more complex for users than what they did with iterators.
Yes! You have to think through and account for edge cases. "Might not" is the same as "might." Those branches might execute.
I have been on many projects over 25 years of software development. Projects that take error handling seriously tend to be better all around. Happy-path coding is naive.
Maybe so, but for JVM languages, this "trivially" includes a stack trace. Go doesn't offer that for normal errors and there was no clear path to getting it. The need for contextual clues when debugging, and the community and then stdlib settling on error-wrapping as the way to do it, makes "simply return err" not a sane default anymore.
It does offer that for normal errors if you pass those errors via exceptions. Although you're right that using exceptions for error handling is pretty weird and generally avoided in Go, much to the chagrin of Java developers.
The Upspin experiment, which eventually shaped the error additions that did make it into Go, showed a clear path to getting it in errors themselves, but it also revealed that nobody actually uses them when available. The research found that even with stack traces attached, developers didn't bother looking at them. Thus why that bit didn't make it into Go.
In a language that takes error handling seriously, you have domain types for errors, type checking for exhaustive error handling, and you get info like a stack trace out of the box.
There is nothing special about an error in Go, and there is no type hierarchy to anchor them against anyway. An error can be a struct{} carrying no value at all, or simple integer with no way to carry context, etc. Error-wrapping is the solution that has been devised to work within these constraints, but there's no obvious way to compose that with the various ideas around reducing boilerplate. For a point of comparison, Rust's anyhow crate was able to square this circle by adding a blanket trait impl for Context, but Go has no equivalent for that. (Also, Rust's ? operator is implemented using unstable components, allowing the details to evolve even while the feature itself is usable in stable Rust, something which also has no equivalent in Go).
As to the lack of discriminated unions, I agree wholeheartedly. There have been some proposals recently to get them into the language, with various levels of conservatism and cohesion with the rest of the language, but a lot of them are faltering on the "every type must have a well defined zero value" issue (yet another design decision that doesn't necessarily have anything to do with errors).
I had to argue about this years ago with a coding standard pushing to define local variables at the top of functions and zero initialize them in C. Why default to null pointers or worse (invalid ints/structures) when you could make use of uninitialized variables be a compiler error instead!?
That may seem out of place in the world you live in, but Go was created in a world where Python, Ruby, and Javascript were the languages all the cool kids were using. Go was intended to be the language for C++ programmers who had to use C++ for performance reasons but wished they could use Python instead.
Insanity, perhaps, but Go was designed to solve a problem for the world it lived in. You don't get to choose your circumstances. For those living in paradise, they can use the languages designed for their world.
Personally, I'm fine with discriminated unions always allowing "nil" in Go, since IMO the most sensible way to implement them anyway is as a special case of interfaces. Not everyone feels the same way, though.
There is no case where you would ever trivially send the error up the stack.
First, if your function doesn't have any meaningful information to add, you need to seriously question why the function exists. It is almost certainly not adding anything of value and the function that originally produces the error can just as well be called directly. No need to trivially pass an error when the useless "middleman" function doesn't exist.
Second, even if you still decide that a function that doesn't have worthwhile information is still worthwhile having, if you pass someone else's error straight through then you become forever coupled to the implementation of someone else. If you, for example, pass a SQLite error and then later decide you want to use Postgres, now you have to emulate SQLite errors every time Postgres produces an error, bringing you back to square one while also having one hell of a confusing API. Unless you hate other programmers and are purposely trying to make their life a living hell, the only sensible approach is to produce new error values at each step so that you can maintain stable sentinels/types.
A good language solution for errors might make it look like you are trivially passing an error up the stack while using some other method to attach the necessary metadata, like we see in some other languages, but nobody has yet figured out how to make that fit into Go. It is generally agreed upon that Go would greatly benefit from this, but until someone rolls up their sleeves and solves it...
You do not need to have new wrappers at every level. That's what interfaces are for. e.g. if all you're going to do is retry up to N times and log, and you don't need special handling, you just call String() or whatever. But you still want libraries to return domain errors so that you can handle them if you need to, or for tests, etc. And you can have some layer in the middle to make like SQLError or DatabaseError.
Right I'm saying Scala does exactly that, and has libraries that e.g. propagate stack traces across fibres automatically and invisibly. There's prior art to observe. It's not some mysterious unsolved problem.
Right, and conceptual tasks will almost always have information that is usefully attached to an error. There are exceptions, but even in those exceptions you still have to ensure you don't leak implementation details, so you still cannot trivially pass the error through.
> Your business process might not care which task had an error
That's true, but you would never write upstream functions with the business process in mind. That is for the downstream functions to determine. Doing so would couple an upstream function to the business process, making it difficult (maybe even impossible) to use again when the next business process wants to reuse it, which would be complete insanity. A function is written to provide what makes the function useful for the sake of the function itself. It is up to the caller to decide what facets of that are useful for the business process being implemented downstream.
> You do not need to have new wrappers at every level.
You absolutely do need them. The language can work to hide that they are being created, but that does not preclude the necessity of them. This is most definitely a solved problem in other languages, but it remains that nobody has figured out how to take those ideas and apply them in Go. What was done in another language cannot be lifted wholesale and simply dropped into Go. The world is not quite so simple. It remains an unsolved problem in Go.
And understandably so. Who has time to work on solving the problem when there are comments on HN complaining that nobody has solved it yet to be made?
Exactly, which said proposal does nothing to improve upon. Despite the reduction in characters it looks exactly the same as the traditional way, offering no way to improve how one think about errors. That proposal was clearly thrown out there just to try and appease the "Go doesn't have error handling" crowd without any thought into what would actually make the language better. The goal may be noble, but that particular proposal was rightfully abandoned.
> The error boilerplate as it exists interrupts the actual logic
Huh? No matter what logic you throw at the problem, there is no escaping that error handling is part of the actual logic. And it is, by far, the most important piece of the logic. Engineering is all about dealing with failure modes.
I'll grant you that there is a category of problems known as scripting tasks where error states simply can mean letting the program crash and allowing the user to clean up afterwards, but Go is clearly not trying to be a scripting language.
My experience with high level banking and low-level networking applications is that the default path for error handling is that you want to bubble up to central handlers the vast majority of the time. There's a reason why exceptions are a popular language feature; they're just hard to make work with fibres. With scripting you often don't want to handle errors at all. Just set -euo.
But looking at how adamant Go used to be on the whole "No Generics" stance and the path and the troubles they are having... it all seemes so preventable?
Go without generics was ok. I say that as a fan of generics in other languages.
There is room for languages with simpler feature sets.
It takes a strong leader (and the good and bad that comes with that) to continue to deny features. Go had that. Does it still? (I genuinely don't know.)
I'm not sure what Go's value proposition is now.
Developers can't pick it up in a week, as it used to be. Maybe that's okay too, since Go already has critical mass.
Well kinda. I found it very annoying every time you want to print a sorted map which keys are strings that you could not abstract the operation. It felt like paying a stupidity tax. (And I quite like go. It’s the only language where write once run everywhere actually works. Cross compiling for all targets is as easy as creating a local binary)
I don't think any of the new additions to the library have seriously made the learning curve more difficult. I suspect that most projects don't use templates at all.
There have always been parts of the standard library I'm not familiar with. when I read code that uses it, I just go read the documentation. Now the standard library is just larger.
Ah, so you haven't looked into json/v2 yet.
As you know from when Go was first broadcast to the world, they hadn't figured out how to make generics fit[1]. It is not like they weren't trying. Ian Lance Taylor is regarded for beginning work on generics before anyone outside of Google had even heard of Go. "In short: not yet"
What was surprising is that after Go was released as an open source project, nobody across the whole wide world ever came up with a workable solution. You can sense the optimism in that original announcement that later turned to "Go might never get generics" dispair. As you'll recall, it finally took convincing Philip Wadler (of Haskell and Java generics fame) to help. Without that lucky break, which almost certainly never would have happened if Go was still some basement project used by nobody, Go probably still wouldn't have generics.
"They are likely the two most difficult parts of any design for parametric polymorphism. In retrospect, we were biased too much by experience with C++ without concepts and Java generics. We would have been well-served to spend more time with CLU and C++ concepts earlier."
https://go.googlesource.com/proposal/+/master/design/go2draf...
But one would think that of the billions of people in the world that someone outside of the Go team would have had that familiarity to quickly change that, but no proposals came from that direction for over a decade and a lot of begging. I suppose those who had the chops were too busy constantly posting on HN about how Go doesn't have generics to lend their expertise. Priorities.
It's great to reflect back in hindsight about where you failed, but it's always easy to see where you failed after someone else finally shows you the way. The simple fact that the Go team did not have the technical capability to be able to add generics at the time remains. Being able to create a relatively popular programming language (or set of popular programming languages for some of them) certainly does not imply that you are a programming language expert. There may even be an inverse correlation. Those who show to be programming language gods appear most likely to end up creating esoteric languages that end up on the pile of languages that remain unknown and unused.
Without the adoption success of Docker and Kubernetes adding wind to Go's sails, Go would have landed on the island.
They were not lacking in skills, rather politics, and there is a public talk from Rob Pike where he mentions even the Go 1.18 generics would not have landed if it was for him.
Exactly. You might say that Wirth, Mössenböck, and Winterbottom had some programming language chops, bringing some new ideas to the world, which is in line with the observation that those with programming language skills end up creating languages nobody uses. In fairness, Pike was involved in Limbo, but his only claim to fame was pressuring Winterbottom to add garbage collection to Alef, thus Limbo.
> Without the adoption success of Docker and Kubernetes adding wind to Go's sails, Go would have landed on the island.
Fun story, but no. Those projects chose Go because Go had already gained traction and was thought, in that moment in time, that it was going to be the way of the future.
It is true that Docker is often credited as the first major success story, but Hugo, etcd, InfluxDB, etc. were all released around the same time, never mind the long list of projects that never gained popularity. The language was already being widely used by the time Docker showed up. Go didn't need a major success story to get on the radar. It had Google's marketing power behind it, or at least that's why most believe it was able to become popular without having any breakouts to its name.
> They were not lacking in skills, rather politics
These are not separable concepts. Politics are the result of lack of skill.
Lots of languages are full of dead projects that never gained notoriety.
In fact, YouTube downloader which is another "success" only happened, because once again, it was a gopher that forced a RIG, it was working perfectly fine.
Ad hominem is a logical fallacy.
> Go was barely proven on the language adoption battlefield when Go heads preverted the direction Docker and Kubernetes were going, it wasn't even 1.0.
Exactly. All these projects chose Go many years before anyone, outside of Docker, had heard of Docker. This idea that Go was some kind of underground language until Docker and Kubernetes arrived at which point everyone jumped on the bandwagon is simply not true. Go was already quite popular by the time Docker was released, as evidenced by a whole lot of other projects that became popular themselves being released at the same time. Those projects wouldn't have been able to have been released when they did if they waited for Docker's success to jump on the Go bandwagon.
> Lots of languages are full of dead projects that never gained notoriety.
No there aren't. Not where the language is on the heap of languages nobody has ever heard of. Oberon-2 and Limbo didn't fail because they never found their "Docker", they failed because nobody used them in the first place.
Small correction, I think:
My understanding is the core Go team was already well on its way to doing generics before Philip Wadler became involved, though he and his colleagues were later very helpful.
From Wadler's "Featherweight Go" paper [1] (the first draft of which seems to have been uploaded to arxiv.org circa May 2020):
Recently, the Go team mooted a design to extend Go with generics [Taylor and Griesemer 2019], and Rob Pike wrote Wadler to ask: Would you be interested in helping us get polymorphism right [...]
and:
It took us several tries over many months to formalise it correctly.
Well before that, doing generics was one of the main focuses in the GopherCon keynotes/announcements in 2017 and 2018 by Russ Cox [2].
The August 2018 generics design [3][4] was very detailed, and my recollection is most people in the Go community at that point believed the core Go team was going to make generics happen, even if the team and community were still iterating on the exact design.
Of course, that design improved over time, including thanks to Wadler and crew, but I think it would be overstating things to say that probably nothing would have happened without Wadler.
[1] https://arxiv.org/pdf/2005.11710v1
[2] https://go.dev/blog/go2draft
[3] https://go.googlesource.com/proposal/+/master/design/go2draf...
[4] https://go.googlesource.com/proposal/+/master/design/go2draf...
It's a very practical simplification since 99% of the time, that's what we use. 'It's in the language, we just don't use it' does sound like copeful technicality, until it isn't and it's an actually impactful difference.
Yes, there are occasionally really good reasons to use an ordered map or a priority queue, but I haven't needed either in years.
The most important "features" of Go are fast compilation, orthogonality of features, and easy development.
Their propositions may "look like everyone else" if you're only looking at surface syntax, but there's a reason why it took 10 years to design an acceptable generics proposal, and the result is that compiling generic-heavy code is almost as fast as compiling plain Go.
Go being strict about features it accepts is not an ideological thing, but a practical one. If Go could have all the features in the world while still being easy to use, fast to compile, and internally coherent, it would. But it can't (see: C++, C#, Python).
Go used very particular shortcuts to make its GC'd runtime lightweight enough and the language simple enough. But, due to Conway's law or something else, it was destined to become a corporate-focused elephantine language, gradually.
I remember 10 years ago I believed that Go lacked something like STL. Later I changed my opinion cause I started to realize how much overhead red-black trees and suchlikes introduce. Especially in GC'd systems. This abstraction Jenga may indeed look shockingly absurd when you see all the moves in the system, not just your layer.
In performance-critical cases, I personally use ABC, a C dialect with solid containers. Those are basically chunks of RAM with fixed-bit-layout records in them. Vectors, hash maps and hash sets, heaps and queues are all just arrays. If we only needed that, we might have stayed with C. Solid containers require no malloc() and no GC. Can mmap them and you have a database. Can send them over the network as-is. That is the ultimate 80/20 thing here. LMDB is another example.
But if you keep adding features, 80/20 is no more and your Go becomes an uglier Java or Rust.
I find it funny reading sometimes how my country learned to conform on something that was previously a bit unique and people are like ”yeah, even they figured out it doesn’t work!”. No, you dingus, the people that wanted to conform were just elected. It worked perfectly fine otherwise.