Hacker News new | ask | show | jobs
by gambler 1516 days ago
Go has a lot of issues. Some of them would be easily fixable if people promoting and developing Go actually admitted the problems. However, the fanbase usually acts as a cult pretending that issues are features. Thing is, just like broken, hackish dependency "management" had to be fixed (introducing tons of complexity for the sake of not destroying backward compatibility), other problems will have to be fixed as well. It will add even more complexity to the language.

For example, Go error handling is shit. People will attempt to fix it with generics now and that will create a lot of inconsistency between different APIs. Because of those inconsistencies the entire language will loose any possibility of elegant, high-level, pre-packaged solutions for certain things (like automated logging, recovery, etc.) This loss will be permanent and the vast majority of users won't even understand why some things are so hard.

5 comments

I mean, all programming language communities are partial to their language, but among Go there seems to be an unusual tolerance for disagreement and discussion of language issues compared to most other programming languages. But yeah, when you come in guns blazing talking about how certain language features are "shit" and there's no possibility of elegance, people are rightly going to think you're not there for any sort of productive conversation.

For example, I regularly have productive conversations with people in the community about error handling and sum types and generics, including my criticism for the way Go does some of those features. A little civility goes a long way, and this isn't particular to the Go community or even programming language communities in general. Note that there definitely are PL communities that generally can't handle any criticism irrespective of civility, but the Go community isn't among them. Indeed, in my experience, Go's critics are very often much more zealous than its proponents.

> Note that there definitely are PL communities that generally can't handle any criticism irrespective of civility, but the Go community isn't among them. Indeed, in my experience, Go's critics are very often much more zealous than its proponents.

This is because Go is a programming language for people who don't care about programming languages. I mean this in the most positive way possible. If you're using Go, it's because you care about the end result of what you're building, usually a backend service or command-line tooling, far more than the code that was used to create it.

Go is not a language where you come up with clever syntax to solve your problem. Go is not a language that makes you feel smart when you write it. Go is not a fun language to program in. Go is a language that gets out of your way, encourages you to solve your problem in the most boring way possible (usually with a lot of for loops) with a predefined level of safety (i.e. static typing, explicit error handling with the `error` type, etc). It's a language for building bridges, not creating masterpieces.

People who are passionate about programming languages would never like Go in the first place, so you don't get too many zealots to sing its praises.

> Go is a language that gets out of your way, encourages you to solve your problem

Maybe I'm just too dumb for Go, but this is not consistent with my experience at all. Go's insistence on pretending that complexity doesn't exist would get in my way all the time. Go's extreme hostility toward FFI calls got in my way several times.

Yea.. i used Go for 5 years. I just can't agree that the simplicity is true. Yes, the language itself is, but it offloads complexity into my program and thus my day to day is jumping around huge piles of logic which could be made for easier to reason about and understand with some actual help in managing the complexity.

Your complex programs aren't easier in Go, in my experience. The simplicity of the language doesn't help me much when my day is spent fighting to figure out how to make a problem which is inherently complex easy to maintain, reason about, and be without bugs.

I want a language that makes my day simpler. Where at the end of the day, my "net complexity" is less. Go leaves all the complexity to you and offers you very few tools to solve this. Bugs, spread out logic, and even runtime costs of the overuse of Interface{} (prior to Generics at least) left me with a lot of things to solve myself. My days in aggregate were more complex with Go.

Just my experiences.

> runtime costs of the overuse of Interface{} (prior to Generics at least)

Generics aren't going to improve this situation - at least not the current iteration of generics :(

It depends. Things like sorting a slice will be faster, for example. But yeah, the current iteration is a bummer.
> Go's inane hostility toward FFI calls got in my way several times.

All languages w/ obligate GC are "hostile" to FFI in some way or another. The Go default implementation also uses split stacks or something for its goroutines, that cannot feasibly interop with FFI code. But it's usually easy enough to just isolate Go code to it's own process/address space and use IPC or network communication to enable the interop one would usually achieve via FFI.

Inversely, virtually all languages with "easy FFI" end up being even more hostile in that a significant chunk of the ecosystem depends on C build tooling which is almost always fragile: C build systems have implicit dependency management, so you don't know what dependencies you need to have installed on your system or where they need to be installed. This means that something which builds on one machine may fail to build on another machine (in the case of build-time dependencies) or that it may run on one machine but not another (in the case of run-time dependencies). It's also opaque to the host build system, so cross compilation becomes dramatically more difficult. Lastly, C is inherently unsafe and insecure in ways that most host languages are not.

In practice, whether by accident or design, the Go ecosystem is really, really nice because it avoids FFI to a high degree. An overwhelming majority of programs can be cross compiled into a truly static binary (it may not even depend on libc unless--as is the case with Windows and MacOS--the host platform requires it). It also means that there are very few "C-shaped libraries", by which I mean thin bindings around some C library which exposes idiomatic C semantics rather than idiomatic Go semantics. Moreover, your programs aren't running a bunch of inherently unsafe code under the hood, and are consequently more likely to be secure as a result.

It's kind of nice that C FFI is possible such that libraries which are unlikely to be ported to Go (e.g., ffmpeg) or which cannot be ported to Go (e.g., opengl) are still available, but not so easy that people pull in C libraries for every little convenience.

The Rust ecosystem does one better and packages the C libraries and build configuration (including making it portable across platforms) as part of the crate. So you just add the dependency to your Cargo.toml and the C library will build as part of the regular `cargo build` process.
Serializing a request structure, making an IPC/network call, deserializing the request structure, serializing the response structure, sending it back, and deserializing it ... isn't really a solution when the purpose of an FFI call is typically to fix some performance issue.

Lots of garbage-collected languages make FFI not only easy but plenty fast. Go does neither.

I started out thinking that fast and easy FFI was ideal and being disappointed that Go's FFI was neither. I've since changed my opinion as it's really nice that one can usually get away without pulling any C dependencies into their dependency tree. I wrote more in the sibling comment: https://news.ycombinator.com/item?id=31194347
> The Go default implementation also uses split stacks or something for its goroutines

This has not been true since Go 1.2, back in late 2013.

The fact remains that you need a separate implementation (cgo) if you want to do FFI. It might be something else goroutine-related that blocks FFI in the default Go implementation, but the issue is still there either way.
> Go's insistence on pretending that complexity doesn't exist

Probably the most accurate and concise summary of my problems with go also.

I am usually unhappy/ worried working in a language or library that pretends the world is simpler than I know it really is. On a good day there is documentation clearly explaining that the maintainers know about the complexity and here's what they've done about that so at least I know; on a bad day it's just shrug emoji.

The article mentions the whole filename thing as an example, and that's one of the first places where I felt I was at home with Rust. It's not unnecessarily complicated but it does force me to acknowledge that yeah, the name of a file might be incoherent nonsense. It's probably a String, but it might not be. I can write code that says "I don't care, we're probably fine" and accept that if it's not fine the code will fail at runtime in a defined way - or I can write code that actually cares about this problem, even if just to explicitly ignore such files as if they didn't exist.

In too many languages the second isn't really an option (which is frustrating if I'd like to write reliable software) or worse, the first isn't an option and so I'm stuck writing endless boilerplate even for a toy or one-shot.

The latter is arguably OK if your language is really just for space rockets and medical implants where failure is not an option. But that's never really how things work out.

Go always seems to be to be designed to be simple for the compiler (which, to be fair, has benefits: fast compilation is useful in a compiled language, to keep code-build-test cycles short) more than the programmer.
Go strives for a balance. It tries to be a fast language without trading off everything else to that end. So it has a GC and really fast builds and it produces machine code that isn't as aggressively optimized as Rust or C++, but it does so much more quickly (as you noted). These are ideal tradeoffs for a huge swath of applications.
I never really understood this reasoning. To me the ideal thing would be a fast debug-compile mode that barely optimizes and a don’t care how slow release mode that uses every possible optimization for the end result.

Rust is plenty interactive with its similar mode of working. Incremental builds are fast.

I am a performance engineer and recommend against this. Optimizations don't really work that well on their own; to get performance you want an ongoing conversation between yourself and the compiler, which you don't get if the compile time is super long.

If you do have a really long running superoptimizer discovering things, then you'd want a way to write that back into the code so you don't need to discover it again.

Also, most of your program should be at -Os because it's not hot code and the important thing is to stop it from disturbing the fast parts. (Or because the aggressive optimizations actually make it slower. Totally possible with fancy ones like autovectorization.)

I've done pet projects in Haskell, Ocaml, Racket, Rust... Now I'm learning Zig... I've worked for years with Java, Python, Javascript/Typescript... Use to work with Z3... Tried plenty of different stuff.

After years my conclusion is that If I want to get a job done I'll choose Golang. Hands down the best productivity programming language nowadays. GC for memory management and productivity, explicit, easy to read, hard to mess up, good performance and efficiency. Get' the job done and really well. End.

I love PL theory. Reading an Idris book an implementing some cool recursive patterns. Building a small project in different languages and compare them... Compilers, type systems and GC papers... But in my experience, the more complexity and "implicitness" a language has to offer, the easier is for "us" to go the wrong way.

I've done all kinds of stuff too, and agree that Go is pretty good to "get things done", especially networking. But I don't know about best. Maybe ten years ago when a static binary was important, but now that everything is deployed as a container, that's off the table and things like Python or Kotlin are equally deployable, but way easier to use. Nowadays, if you _really_ need a single binary, you probably also need it to be tiny. Cramming an entire GC and runtime into the executable doesn't seem much different than building a container to me.
God no not Python. It’s easy to use but it’s something I would only put out there as a duct tape or personal use solution. If I know someone is going to have to look at it after I’m gone, I need something heavily opinionated and without too many syntactic sugar that slows down the refactoring/debugging process.
I've been developing Python professionally for 15 years, including almost a decade of deploying to containers. I think Go is much easier to use (especially in a container environment):

1. Static types make it much easier to read and write code for even a single individual, and the benefit scales superlinearly as the contributor count and code base age increase. Go also has a ton of other tooling which just outclasses Python equivalents for both simplicity and performance (e.g., profiling tools, and even things like gofmt vs black where the former is way faster)

2. Because Python is so slow, even medium sized test suites take a long time to run. You end up having to triage your test suite to keep CI times reasonable. This just isn't a problem in Go (unless you're doing something I/O bound).

3. Python dependency management still sucks. If you want reproducibility, it takes ~30 minutes just to resolve dependencies for relatively small-but-not-toy-sized projects. This obviously kills your CI times, and there aren't great workarounds except to forego dependency management altogether. Go builds are nearly instant in most cases (assuming you have build caching enabled in CI) and still far better than Python builds in the worst cases. Python also depends a ton on C, so cross compilation is basically impossible (whereas it's trivial in Go) and simply building for any non-mainstream platform is going to entail a whole bunch of work (C projects typically make sweeping, undocumented assumptions about their build environments and targets).

4. Being able to make small artifacts is surprisingly important. When your container image is hundreds of megabytes, you feel it in your iteration loop (especially if you're in a "site down" situation and your iteration loop involves rebuilding and deploying containers to production to restore service). It also means your services can't scale up as responsively, and if a container gets bounced (and scheduled onto another node) it implies longer downtime before that container can carry load again. Similarly, rolling back from a bad deploy can be almost instantaneous if your images are small. Go has the advantage here because it can build on scratch images and because it doesn't need to ship the complete source code (native compilation prunes unreachable code, and binary machine code is considerably more compact than unicode source code).

5. If your development environment is Mac or Windows, Docker kind of sucks for Python development because you'll want to mount your source code volumes into the container, but Docker for Mac/Windows runs the container in a Linux VM with a process that marshals filesystem events back and forth over the guest/host boundary consuming virtually all of the CPU allocated to the VM. In Go, you don't mount the volume at all, rather you just build the image from scratch or you rebuild the binary within the image (or outside of the image and copy it in). You can viably use something like `docker-compose build` as part of your iteration loop with Go.

6. Distributing CLIs via container images makes for a crumby end-user experience, and if you don't distribute Python via container images. Something like shiv mostly works, but there might still be dynamic dependencies that users have to include (iirc, we ran into this with graphviz and a few other libs). Go binaries Just Work.

> Cramming an entire GC and runtime into the executable doesn't seem much different than building a container to me.

A Go runtime (which includes the GC) is just a couple of megabytes. Slim python base images are 60mb compressed.

> If you want reproducibility, it takes ~30 minutes just to resolve dependencies for relatively small-but-not-toy-sized projects.

I will be the first to complain about Python packaging, but 30 minutes is far far beyond anything I have experienced.

Well put and I agree with most points but

> Go is not a fun language to program in

Not having to think about how something should be done in the most elegant way, instead focus on the problem at hand is a lot of "fun"

Having to write the same code several times with minor changes because of a lack of abstraction is a lot of fun.
I don't know how people can say go "gets out of the way".

Go makes me write dozens of lines of code to do something simple that in an any modern language takes a few.

It doesn't get out of the way, it gets in the way constantly. I'm constantly thinking in any modern language I can just do X, but in Go with its myriad missing features I have to sit and think about how I'm going to do it with just loops and if statements.

It's the exact opposite of getting out the way, don't even get me started on the syntactic verbosity.

> Go makes me write dozens of lines of code to do something simple that in an any modern language takes a few.

"Getting out of the way" doesn't mean it takes fewer keystrokes - it just means that you don't have to think about it / there are no surprises. It took me a while to grok what pythonic code is and looks like, and I feel the bar for Go is even lower. Even if you're browsing an unfamiliar codebase, code is exactly where you expect it to be, and you don't have to ponder on where to make your changes. To me, that is how a language moves out of the way; it fades into the background and you mostly concern yourself with the logic.

Most programmers aren't bottlenecked by keyboard proficiency, but rather by dealing with poor tooling or gratuitously complex programs ("terse" doesn't entail "simple", and very often it's the inverse).
True, fun is certainly different for everyone! I also enjoy being able to just focus on a real world problem, but I also programmed Scala professionally for many years, and I found it a lot more fun purely from the point of view of writing code. Writing a really elegant for comprehension or using currying in clever ways to make your code "elegant" was just enjoyable in and of itself, regardless of what problem you were actually trying to solve. Rust is pretty similar to me in that regard.
Same. I use Go for work because that's what the company uses, but I can't imagine coding in it for "fun". Elm + Scala feel much better to me.
Agreed. This was the only nit I was inclined to pick as well. I have a lot of fun writing Go, because it gets out of my way.
I just can't stand taking three lines to unpack a value from a map or to return if error.

Why can't I just say `return if err := somefunc(); err != nil`

It's mega frustrating on top of the lack of generics and other abstractions.

And now that generics are coming about, I'm sure it will take forever until my current project can use them. My current project is in the k8s ecosystem which due to the lack of generics, implemented its own clever but awful type system.

I can't relate. Newline characters have never been burdensome to me, and they aid in visual structure (the control flow is represented by the visual structure of the program, not only for "good data" paths, but also for error paths). My programming problems are usually not related to localized keystroke boilerplate, but rather larger issues of abstraction and data modeling.

> My current project is in the k8s ecosystem which due to the lack of generics, implemented its own clever but awful type system.

The k8s ecosystem's type system is unrelated to generics. It has a concept of user-defined resource types, which means that users can provide an OpenAPI document describing their resource type which Kubernetes will then use to validate new user-provided resources of a given type. From the perspective of the Go compiler, these types are dynamic types--they can't be known at compile time. They aren't a candidate for generics in the host language.

That said, it's often tedious to write a controller for these resource types, but that's because Kubernetes' controller frameworks are really complicated. They remind me of enterprise Java code with gratuitous abstraction. Maybe that abstraction serves some purpose, but it wasn't helping me and I ended up rewriting much of it in more standard Go (I didn't release it because it was prototype code and I didn't want to support it) and it was quite a lot simpler. I don't recall seeing many places where I felt that generics would be a significant improvement, but it's been a while.

The problem here is that you think "error handling" is somehow different, and probably less important, than normal logic in your codebase. But Go asserts that the "sad path" is just as important as the "happy path".
But programming languages should get on your way while you're doing things wrong. Go does not. To be fare, most mainstream languages do not: I think Rust is the best in this thing, other languages often aren't. But Go is by far the worst of all, because of its striving for "simplicity".
> But programming languages should get on your way while you're doing things wrong. Go does not. To be fare, most mainstream languages do not: I think Rust is the best in this thing, other languages often aren't. But Go is by far the worst of all, because of its striving for "simplicity".

Go typically does get in your way when you're doing things wrong, but yes, I'd like to see Go require return values be dealt with or explicitly ignored. That said, there are linters for this, but in practice it's never been a material problem for me so I haven't bothered to wire one into my project. Over time, I've learned not to be so concerned about issues which are mostly just theoretical--there are enough practical problems to deal with first.

Seems like the opposite to me. I once tried to set up and run a GRPC service (I can't remember which one) but something it depended on changed and so the codebase I was trying to run basically didn't run anymore. It was f-ing weird that (at the time?) there was no way to lock down deps - that or someone didn't care to? I don't know the language baffles me completely.
There has always been some way to pin dependencies, but historically you had to opt into it via vendor directories and the like; however, as of the last ~4 years, the standard project format manages this for you (the go.mod file pins dependency versions).
> Go is a language that gets out of your way, encourages you to solve your problem

Haven't experienced this yet but I'm a Go noob. I think everything looks easy when you mastered it, I don't think Go is so much easier than JS/Python or even C. Might be easier than Java but Java has so much more community support (e.g Stackoverflow answers) it easily evens out.

I started playing with Go in 2012 when I was doing professional C, C#, C++, Java, and Python. I stuck with it because almost everything was surprisingly easy. For example, I didn't have to learn an obscure DSL just to include dependencies! I didn't have to figure out how to wire together a "test target" in that DSL or evaluate a dozen test frameworks to get unit tests running! I could build and deploy a high-performance HTTP server with a single binary (no external apache/uwsgi/etc web server process)! And often without any third party dependencies at all! And idiomatic code ran 100x faster than Python, and on top of that there was headroom for minor optimizations (pulling allocations out of tight loops, basically). After a bit of experience, it as even easier than Python or JS thanks to static types.

> Might be easier than Java but Java has so much more community support (e.g Stackoverflow answers) it easily evens out.

This was true in the early days, but now Go is extensively covered in Stack Overflow. Of course, there aren't as many Go posts on SO as there are Java posts, but that's because Go is considerably simpler--there's less information to cover.

This just sounds like the same arguments that have been made for C for ages. The complexity of programming is pushed out from the language into the program, the tools, and the programmer’s head. It’s not often that that’s a worthwhile trade-off
Huh, I had sort of found the opposite, that the Go community I had interacted with was more aggro and prone to offense. I'm forming this opinion from the reddit and the discord though, so if there is another community you favor I'd genuinely love to hear about it.
I concur. I was used to a professional tone, then joined discord-go. Showed some of my online code to receive a "shit structure" response by some anime girl avatar youngling. When I started explaining how I don't think docker is the way to go I was met with passive aggressive behavior and plain false responses trying to justify its use. But Go's discord server wasn't to only bad experience. Angular might as well have been called Heil Angular. I worked with it for 2 years and found vue to be more productive. But they didn't want to hear cross worlds experience. Instead they insisted that I had no clue and after a while of back n forth played the "we're the moderators so we're right" card. Then big daddy server owner later stepped up and in the end I was banned, because despite the truth that vue was more productive only their agenda mattered. I have also met hostility in discord's vue server. Insults by by an official name there for saying that it wasn't a good move to require me or anyone joining to provide a phone number for account verification purposes.

All in all discord seem to have the immature unprofessional crowd. It's a gaming chat system after all.

Reddit Go is not as hostile but not very informed either. Although that's not true for all participants.

Compare Reddit to the quality of Go nuts, there is a difference.

But at least Reddit is a more or less open forum, where discord is hidden and a walled pff property.

You can't base your opinion on anything, particularly computer languages, off of your Discord experience, come on.

The Discord demographic is teenagers and young adults, that's the last place where you'd find professional and mature advice about a programming language. I mean, even Reddit is better, and it still is a cesspool.

I've found the rust discord nice and respectful (although I'm also a young adult). I also don't think [edit: PL] reddit is particularly a cesspool, at least compared to HN.
rust discord is at least (semi?)official go discord isn't listed anywhere on official sites
I've had very helpful experiences on Discord (but I don't frequent it). Reddit is Reddit. GitHub (e.g., issue tracker) has been very productive. The mailing list is also productive.
Last I checked there GoNuts on Libera, there's the go nuts mailing list, and there's that Slack. I've seen members of all of the above complain and publicly vye for features and fixes.
> However, the fanbase usually acts as a cult pretending that issues are features.

Per Rob Pike (Lang NEXT 2014), golang was created for fairly young programmers that are fresh out of school and don't know many other languages.

So, something I've observed: When somebody doesn't know many things but is building a career, planning their life, on one of the things they know, they're going to take that one thing more personally. This is why it's good for people to be exposed to a diversity of ideas early on. Early exposure to diverse ideas helps engineers reason about tools and systems more objectively, with less input from their ego.

Do you have any data or survey to back your statement? Forgive me if I've misunderstood you but are you saying that Golang is mostly used by young programmers?

In my experience most Golang developers are highly experienced... Same with Rust.

Yes, you just needed to search for Lang NEXT 2014 and Rob Pike.

Enjoy the video, https://youtu.be/YM7QYx-LPSA

"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."

Or if you prefer reading, https://talks.golang.org/2012/splash.article

"It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical."

> https://youtu.be/YM7QYx-LPSA

That link is to a panel Rob Pike participated in at the same conference. I'm not sure if he makes similar remarks during that panel, but that "fairly young, fresh out of school" quote specifically comes from Rob Pike's presentation at the same conference titled From Parallel to Concurrent, which you can watch here: https://www.youtube.com/watch?v=iTrP_EmGNmw

You're right, thanks for the correction.
There are two parts of my comment. The first part paraphrases what Rob Pike said about the purpose of Golang, in his presentation at Lang NEXT 2014:

> "The key point here is that 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 be able to use them to build good software. And so the language we give them needs to be easy for them to understand and easy to adopt."

The second part is based on my personal observations of human nature. Young and relatively inexperienced engineers often form a sort of personal attachment to whatever technology is enabling their new career. With this personal attachment comes a perception of attack against their person when that technology is criticized. This is a broad phenomena, not unique to golang by any means, but golang happens to be one of the languages that is popular with and promoted to young engineers. In discussions critical about golang, or javascript, or C, or python, there will often be young or otherwise inexperienced engineers interpreting criticism of the tool to be personal attacks.

> the fanbase usually acts as a cult pretending that issues are features

JavaScript kind of went through the same thing a few years ago. While everybody else was complaining about Callback Hell, the JS guys were insisting it wasn't a problem. Then they added promises, and later async/await. And lo and behold, what wasn't a problem eventually got fixed.

For a while you would constantly find folks on forums saying, "yeah JavaScript used to be shit, but with ES* it's now perfect." This went on for years.

I think the cultish "we like it this way" is just basic human programming. We do it with items we purchase, political parties we've joined, cities we live in, programming languages, everything.

“The JS guys” is not a thing. I want to say that grouping everyone that uses JS into a single entity is a bad thing, but it’s not even clear whether you’re referring to JS users or the shadowy JS powers that be.
> For example, Go error handling is shit

What is bad about it?

It requires several additional lines of code just to bubble up an error, for starters, and there's nothing stopping you from ignoring errors and continuing with what could easily be corrupt data.
"there's nothing stopping you from ignoring errors and continuing with what could easily be corrupt data."

In theory, this is a big deal.

In practice, it doesn't seem to be a problem. I've neither hit this very often myself, nor have I seen even newbies have much problem with it.

A lot of error handling procedures are based on reacting to C, which was awful. You could call a function, and then have to call another function, deliberately, to see if it failed. This is a nightmare, absolutely. A "Result" type does indeed solve the problem, but the fact that it solves the problem doesn't mean it is the only solution to the problem. The Go solution seems to be about 99.99% effective. It isn't a 100% solution, no, but by 99.9% or 99.99% or so, it takes it below the level of problem that I care about.

The issue with "bubbling" is a bigger problem in practice, certainly.

> A lot of error handling procedures are based on reacting to C, which was awful

Go's design decisions in general make a lot more sense from this perspective. "X was horrible to deal with in C, how can we make a (reactively) better version?"

The problem is a lot of these choices (willfully?) ignore the decades of language innovation that have happened since C. They are incremental, reactive improvements, where it doesn't feel like the designers necessarily stepped back and looked at the bigger picture

> it doesn't feel like the designers necessarily stepped back and looked at the bigger picture

I get the sense that the designers have a huge amount of ego from making C what it is today.

I've ran into bugs multiple times because I ignored an error result, or overwrote the "err" variable and swallowed an error. Errcheck helps a bit.

Other languages that have exceptions that bubble up the stack have a few advantages (easier to instrument with monitoring, stack traces and line numbers out of the box) but developers often misuse error handling as flow control

> I've ran into bugs multiple times because I ignored an error result, or overwrote the "err" variable and swallowed an error. Errcheck helps a bit.

Any reasonable code review process would catch these (very obvious) problems.

my golang code is littered with err handling etc, it’s easy to miss something over the course of dozens of prs. This is something best caught at compile time or by a linter. Or tests which I am often lacking

I find code review unreliable at best for catching bugs or logic errors, but depends on the reviewer

In practice, it's been a pretty common problem at organizations I've worked in.
The latter is a much stronger argument than the former (no idea why people get so worked up about character counts), but even then, "shit" is really strong considering how often one experiences exception traces when using an application written in Python or Java or some other exception-based language. Point being, we should probably evaluate error handling schemes based on results rather than ideology (even though I tend to agree with some of that ideology).
Screen real estate is limited, especially vertical real estate. Compared to languages with saner error handling, I can read approximately 25% as much Go code at once. That's a real cognitive burden when maintaining code or learning your way around a new codebase, which seems especially egregious from a language whose community consistently proselytizes about how the lack of language features is great for maintainability and onboarding.

I'd take exceptions any day of the week over Go's solution. I'd much rather the program crash by default than attempt to continue with corrupt data by default. I'd rather have concise, explicit, compiler-required error handling than exceptions, though.

I don't think "screen real estate" is the right argument here.

The problem is just that every line creates cognitive load and there's a tradeoff between concision and descriptiveness.

A language with piles of syntactic sugar and magic gets it wrong with too much concision and can read like line noise when it gets overused.

Go goes the other way though and makes it way too verbose and just makes it difficult to read the code. When a method needs to have 6 different error handling clauses in it, then it isn't as clear that 5 of those just bubble up the error while one of them has some unique handling. It also increases the chances that some programmer copypastas the boilerplate bubble-up code to all six of those cases and it sails through PR review. You can write a static analysis linter to force programmers to always handle errors and not ignore them, but you can't force them to handle errors correctly. When humans are reading the code, concision helps and verbosity hurts -- up until that crossover point where magic causes readability to suffer.

Go programmers seem to focus on abhoring magic and rejecting the benefits of concision. But when it comes to PR review your job is to stare at the whole method (or the whole file) and be able to "see" the bug, and more lines of code will make this job more difficult (and is also why some of the recommendations of the "clean code" book are pretty bad since extracting more tiny little methods can harm overall readability). There's a happy optimum somewhere where cognitive load is minimized. That isn't attained though by just having the simplest language design possible and offloading complexity into more verbose code.

I disagree. IMO, there's much more cognitive load in parsing dense, "minified" code than there is in scanning code whose control flow mirrors its visual structure. Humans are very good at seeing visual structure (which is why we tend to indent, split code across lines, and other syntactically insignificant usage of whitespace). By convention in most mainstream programming languages, this visual structure mirrors code flow, so we can see the control flow at a glance; however, many languages have special hidden control flow (exceptions) or control flow which is otherwise isn't part of the visual structure and thus easily overlooked at a glance (e.g., Rust's `?`). In my opinion, this "hidden" control flow allows more errors to slip past reviewers (though some languages might recoup some quality by other means).
Cognitive load is unrelated to SLoC.

This expression

    let a = x.iter().filter(...).apply(...).map(...);
is equally or even potentially _more_ cognitively complex than this expression

    for _, v := range x {
        if !filter(v) {
            continue
        }

        vv := apply(v, ...)
        vm := map(vv, ...)
        ...
    }
> Screen real estate is limited, especially vertical real estate. Compared to languages with saner error handling, I can read approximately 25% as much Go code at once.

In my experience, people can't actually read everything on the screen at one time anyway, and the more dense/terse things are the harder it is to read (otherwise we would minify everything).

> I'd much rather the program crash by default than attempt to continue with corrupt data by default.

It's not likely that it will continue with corrupt data because you can't use the return value without explicitly ignoring the error. It's not perfect, because there are cases where you want to crash when there is an error but no return value, and Go doesn't help you here. I would like to see this improve, but it's relatively low on my list of qualms with Go (I would rather have sum types, for example). It certainly isn't worth changing languages over especially since, in practice, Go seems to have fewer error handling bugs than exception-based languages.

> In my experience, people can't actually read everything on the screen at one time anyway, and the more dense/terse things are the harder it is to read (otherwise we would minify everything).

Whether or not you can read everything on the screen at one time is missing the point entirely. The point is that context matters, and the more frequently you have to scroll to find it is more cognitive burden.

> It's not likely that it will continue with corrupt data because you can't use the return value without explicitly ignoring the error.

It is far too easy to accidentally do the wrong thing with an error in Go. In Rust, for example, no matter what you want to do with the result of a fallible call, you have to do it explicitly. If you want to crash on error, you `.unwrap()`; if you want to bubble it up, you `?`; if you want to continue with a default value, you `.unwrap_or()` or one of its variants.

> in practice, Go seems to have fewer error handling bugs than exception-based languages

This is based on?

Additionally, the more repetitive code there is, the more opportunities there are for some subtle difference to be lurking in one particular chunk. And with pervasive boilerplate it becomes easier to eyeglaze past that subtle difference. Whether that difference is a bug or intentional, it's important to have code that highlights it by default.
> Screen real estate is limited, especially vertical real estate.

meh, my ide squashes short if `err != nil` clauses (goland, but I've seen other editors/ides/golang plugins do this as well ), also I run a vertical monitor. It's just not enough of an issue to care about. I've seen similar features for editors for other languages that have features or patterns that also create a lot of 'extra bullshit that takes up screen realestate'.

> no idea why people get so worked up about character counts

Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.

You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages. Quantity has a quality all its own, and Go does bog you down with its sheer line count. I'm not one who often appeals to this argument, by the way; the only other language I've done significant work in that I would apply it to would be C. It's very common to write bloated, information-poor Java code, but that is still a choice, even if it is the most popular one.

My reaction looking at Go initially was that it was exciting to have a fast, simple language designed for writing services. My reaction to reading and writing code of real applications has been that Go is badly suited for writing nontrivial application logic.

> how often one experiences exception traces when using an application written in Python or Java

Python and Java aren't particularly ambitious standards for a 21st-century language.

> Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.

Reading code and mining have nothing in common. In particular, mining technology works best on dense ore, human visual perception requires whitespace to operate efficiently.

> You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages.

I think Go is easy to read because (1) it ranks at the top in my experiences with other languages and (2) because humans are very good at scanning visual structure and less good at parsing arbitrary syntax. Most languages tacitly acknowledge (2) by way of indentation and other syntactically irrelevant whitespace, but they don't apply the same rigor to error handling.

> Python and Java aren't particularly ambitious standards for a 21st-century language.

I was remarking specifically about exception handling. Has there been much innovation in exceptions among 21st-century languages?

I don't think the difference between Go and more expressive languages is about whitespace. If the only way a language achieved fewer lines of code was by cramming more characters into a line, I wouldn't give it any credit for that..

> Has there been much innovation in exceptions among 21st-century languages?

Yes, there has, mostly in the ability to use exceptions less than previously. With Java (at least old-school Java, not sure where it is now) exceptions are the only type safe language-supported way for a function to terminate with multiple types. If a function has multiple possible return types that don't have an inheritance relationship, you can choose between 1) returning Object and dynamically checking for the specific types, 2) defining a return class with a field for each possibility, or 3) picking one type to be the "expected" outcome and defining all other outcomes as exceptional. With 1) you lose many of the benefits of static type-checking; with 2) you get code bloat; with 3) you get the hazards of using a nonlocal control mechanism when you don't want that power.

If your language has sum types, you have a better option for those situations.

Cognitive complexity is not related to character count.

The expression

    let x = f.a()?.b()?;
is exactly as "easy" to parse as the code block of

    y, err := f.a()
    if err != nil {
        return fmt.Errorf("a: %w", err)
    }

    x, err := y.b()
    if err != nil {
        return fmt.Errorf("b: %w", err)
    }
They are effectively equivalent in terms of cognitive load.
I disagree. These two bits of code cater to different ways of reading. The first caters to a happy-path reading, where the reader has the choice to yadda-yadda the error handling or mentally expand it. The second foregrounds the error handling on an equal footing with the other logic.

I like your example, because this is exactly what happened in the application code I had to work with. In an application with complicated business logic, it isn't just one line of code turning into ten like you have here. It's ten lines of business logic turning into forty or fifty, where each operation is separated from the next by multiple lines of error-handling boilerplate.

The trade-off is that in Go code you can see every error path. This is a good trade-off for systems where absolute reliability and rigorous error handling are critical.

In an exception-oriented language the error paths are often invisible. This is a good trade-off for complicated business logic where error handling usually means aborting with an appropriate exception. Think about processing a record or a request where you have to validate the request, look up a few related objects in a datastore, check some business rules, do an authorization check for the requesting user, calculate the result of the request, store the result in a datastore, and produce a response. Each step can be written in a couple of lines of code that are hopefully pretty understandable if you have good names, like this:

    validate_request(request)
    user = fetch_user(request.for_user_id)
    authorize_user(user, Privileges.CanFoo)
    dingles = fetch_dingles_by_dongle_group(request.dongle_group_id)
    unfooable_dingles = dingles.filter(not_fooable)
    if (!unfooable_dingles.is_empty()) {
        throw BadRequestException("This dongle group contains unfooable dingles")
    }
    fooment = calculate_total_fooment(dingles)
    fooment_store.save(fooment)
    return DongleGroupFooed(request.dongle_group_id)
From one point of view, this code is nice. You can read these lines of code quickly and see what the basic request handling logic is. It reads like a story.

From another point of view, this code is terrible. A lot of different things can go wrong here, and only one of them is visible. What happens if the user can't be found? What happens if the user isn't authorized? What happens if the dongle group id can't be found? If the wrong exception is thrown, the wrong result will be reported for this request. You have to navigate to other functions to check that. If that makes it bad code for you, then you'd probably rather be writing Go. In Go, these eleven lines would turn into thirty to fifty lines of code. The handling of each error would be visible, at the cost of the happy path being harder to follow.

The solution space here isn't "go vs exceptions," it's "errors as values vs exceptions (vs "let it crash" vs...)," and Go isn't the only implementation of errors as values.
Of course. I wasn't trying to imply anything to the contrary. But given the prevalence of exceptions, if Go's error handling is performing on par or better, then it seems pretty ridiculous to characterize Go's error handling as "shit". Don't worry Steve, I think Rust's error handling is pretty cool!
I do agree that exceptions feel like the worst of the various bits of the problem space, to me, but just to be extra clear about it, I have never written a significant amount of Go, and therefore don't really have a very strong opinion about its error handling.

And error handling is such a huge and interesting problem space! I've long wondered about why I didn't like checked exceptions in Java but do like errors as values, for example.

Character count does matter, but I still don't see how it makes code more readable if all the significant operations are supposed to happen in the condition block of an if statement.
> It requires several additional lines of code just to bubble up an error

I mean people hate exceptions for a reason (other than in Java world).

And what is that reason?
Errors as return values is only acceptable for code that is so performance-sensitive that you aren't allowed to do dynamic memory allocations. For everything else, conditions+restarts are the correct answer, because errors-as-values restricts you to a single error-handling strategy and couples high-level code to low-level code as a result.
> Errors as return values is only acceptable for code that is so performance-sensitive that you aren't allowed to do dynamic memory allocations.

Exceptions-based code can be zero-cost, if no errors occur, at an increased error-case cost. Using error values pessimises this, and increase branch prediction load (as every callsite is now a branch).

So in the common case where errors are extremely rare, exceptions-based error handling can be quite a bit faster than return-value error handling.

Which doesn't mean it's preferable, but beware thinking that return values are faster.

This is an extremely good point - thank you for your correction!
Fun history fact: Rust had conditions, a very very very long time ago, but folks didn't use them and found them vaguely confusing, so they were removed.
This doesn't mean very much. Most people find the Rust borrow checker "vaguely confusing" (if not very confusing), and most people also wouldn't use it were it not strongly suggested by both the compiler and the community ("suggested" as unsafe Rust exists, but you of all people are aware of that).

Conversely, I understand condition systems, and I'm not a very good programmer. (I've tried and failed to learn Rust once already) That's a pretty low upper bound on how hard they are, especially relative to advanced features of languages like Haskell.

We're very fortunate that programming language design doesn't advance solely by giving people more of what they already use.

I don't mean to say anything about conditions generally, just to mention that they have existed in non-Lisp languages on occasion.

> We're very fortunate that programming language design doesn't advance solely by giving people more of what they already use.

Agreed!

Doesn't adding conditions/restarts require your language to have stackful continuations? Those are quite complicated unless you don't bother to make them safe (like C doesn't bother to with setjmp/longjmp). I would be nervous about them for the same reason I am about exceptions.

Also, the restart seems to imply that you should "handle" the error, but I think this is really overemphasized cause what are you actually going to do about it? There's nothing to do about a lot of errors except die, but this encourages programmers to just make something up they think might help.

> Errors as return values is only acceptable for code that is so performance-sensitive that you aren't allowed to do dynamic memory allocations

Not really. It is acceptable for code whose maintainers value readability and simplicity over everything else. I totally agree that readability and simplicity are quite subjective and this is up to the maintainers.

I don't really know what "conditions+restarts" is but a few articles landed me into LISP which I find totally unreadable. So, can you point me to some "conditions+restarts" code that I can understand/appreciate easily? Any language is fine, I just want to understand the concept better since I am more a C/C++/JS programmer. (FWIW, never written Go, but it's easy to read and understand).

It's probably sufficient for this conversation to just understand it as try-catch. A function is invoked; if it "signals" (throws) then control moves to a handler that matches the signal (exception); the handler runs and resolves the situation. Of course, Lisp being Lisp, the system is extended to announcer voice FULL. GENERALITY. but in its simplest form it's basically equivalent to exception throwing.
This is correct (in that the most simplistic case is try-catch).

However, the difference between a try-catch and conditions / restarts is that when one signals a condition (exception), the restart (catch) has a continuation from the condition. This allows you to inject an expression into the location where an exception occurred and "restart" your code from that point.

Whether you do such a thing or not depends on the code, on the type of condition raised, and on what expressions are valid. So you get a lot more flexibility in how errors are handled across the system. But likewise: more complexity in having to make that choice in the first place.

Going farther than this, conditions and restarts are really just a fancy way of packaging delimited continuations. I don't personally know any non-Lisp language that has attempted to package these concepts (maybe Dylan, which is a Lisp-like in its own way but without the syntax?). Going back to the original thought regarding error handling - I think Result<T, Err> type handling is fine and that most languages would be better served by that than having different types of exceptions. Conditions and restarts are powerful but your language has to be very expression focused (i.e. does not use a lot of statements) and it's not really clear that there's been a lot of work on making restarts nice to use. Exceptions in all languages that have them have their own set of associated problems, for what its worth, and it's not as easy to move Lisp features into a non-Lisp as one might believe...

> Going farther than this, conditions and restarts are really just a fancy way of packaging delimited continuations. I don't personally know any non-Lisp language that has attempted to package these concepts (maybe Dylan, which is a Lisp-like in its own way but without the syntax?).

Dylan does have a condition system, but it’s basically a Lisp without the parens, so probably doesn’t count. On the other hand, algebraic effects are another fancy way of packaging delimited continuations, so arguably the research languages Eff[1] and Koka[2] tried. (I don’t think either one explored the connection with condition systems, but I’m not sure.)

> I think Result<T, Err> type handling is fine and that most languages would be better served by that than having different types of exceptions. Conditions and restarts are powerful but your language has to be very expression focused (i.e. does not use a lot of statements) [...]

Huh? I don’t know why you’d say that, if anything I think it’s the Either err t / Result<T, Err> style that is more expression-focused (I mean, it even originates in Haskell :). I wouldn’t even call Common Lisp particularly expression-oriented, honestly, not unless we’re comparing with plain old C and not Rust.

[1] https://www.eff-lang.org/

[2] https://koka-lang.github.io/

> It is acceptable for code whose maintainers value readability and simplicity over everything else.

Errors-as-return values are less readable than conditions, not more - there's literally more visual noise on the screen.

And if you want "simplicity", don't use a computer. Computers are intrinsically complex devices, users desire features with complex implementations, and our job as programmers is to manage complexity, not pretend that it doesn't exist. One of the article's main points is that Go does the latter in lieu of the former, and that's also what errors-as-return-values does.

----------------------------------

The formal name for a condition+restart system appears to be "algebraic effects"[1].

Conditions and restarts are similar to exceptions, with the following changes:

First, conditions are conceptually used for non-error conditions in some cases, like what Python does.

Second, throwing a condition doesn't cause the stack to unwind up to the handler, unlike exceptions.

Third, in addition to throwing conditions, you, uh, wrap ("establish" is the jargon used) code in what are called "restarts", similar to wrapping things in try/catch blocks (but distinct, because with conditions you still have condition handling blocks). Restarts can have names and are non-mutually-exclusive. Conceptually, restarts represent error-recovery strategies, while conditions represent the errors themselves.

Fourth, when a condition is thrown, it propagates upward until it hits either the toplevel (in which case the interactive debugger is launched), or it hits a condition handler - without unwinding the stack. Then, either the human looking at the debugger can pick which restart they want to use, or the logic at the condition handler can do so.

Why is this better than any alternative error-handling mechanism? Because every other error-handling mechanism (1) unwinds the stack (destroying all contextually useful information that isn't explicitly saved by the programmer, and preventing you from restarting a computation in the middle) (2) forces you into a single error-recovery strategy and (3) couples low-level code to high-level code as a result.

In general, low-level code has details about the specific kind of error, context around it, and access to data and control flow that would allow the error to be recovered from (e.g. for a log-processing program, reasonable restarts while parsing a log entry would be (1) skip it (2) retry (3) use an alternative parser and (4) return an empty entry), while high-level code has the application context about why the low-level operation is being performed in the first place and which error-recovery option should be picked.

Conversely, high-level code doesn't have details about what the low-level code was doing at the time of the error, and low-level code doesn't have the high-level context necessary to determine which error recovery strategy is appropriate in this use of the low-level code.

[1] https://en.wikipedia.org/wiki/Effect_system

https://en.wikipedia.org/wiki/Exception_handling#Condition_s...

> Errors-as-return values are less readable than conditions, not more - there's literally more visual noise on the screen.

No.

When you make a function call, and that call can fail, then the happy-path and the sad-path are both things that you need to manage as a caller. Happy-path and sad-path are two equivalent states that both need to be accommodated by the program logic.

Error handling code is not "noise". It is equally important to success-path code.

> No.

Yes. There is literally more visual noise on the screen. This is not up for debate - more pixels are lit on the monitor you are looking at.

> When you make a function call, and that call can fail, then the happy-path and the sad-path are both things that you need to manage as a caller.

False - the direct caller is not responsible for error-handling, in general - some transitive super-caller will be. Errors as return values needlessly generate this visual noise for every caller, when not needed, in addition to introducing aforementioned coupling.

> Error handling code is not "noise". It is equally important to success-path code.

You're misunderstanding my point. I never said that error-handling code is noise - it isn't. What is noise is forcing every single function call between the appropriate error-handling point and the error location to have extra useless junk. When there's an error, you should see exactly two things in your codebase: some stuff at the point where the error is thrown, and some stuff at the point where the error is handled - and, given that the place where the error should be handled is rarely the direct caller, you should see nothing in between.

Maybe this is the fundamental argument here. Plenty of cases I have written code where I am trying to do something over a big set of things, e.g. check a file for hard-coded paths, or send a message to a lot of people; it isn’t weird for those things to fail. Maybe I couldn’t open the file. Maybe the file lacked hard coded paths. Maybe the sender lacked rights to send to that receiver, or maybe the receiver is currently offline. But if most of your code is some complex calculation, say weather simulation?, maybe there is by default just one path.
> I don't really know what "conditions+restarts" is but a few articles landed me into LISP which I find totally unreadable. So, can you point me to some "conditions+restarts" code that I can understand/appreciate easily?

You’ll have to read Lisp, I’m afraid; the best description I know is in the book Practical Common Lisp[1].

(Come on, Lisp syntax is quirky, but it’s not unreadable, and unlike APL or Forth or even Haskell it doesn’t require you to memorize a bunch of semi-meaningless punctuation before you can understand what is going on—it’s pretty wordy usually. I’m not saying you must bring yourself to love writing (f x y) instead of f(x, y), only that adjusting from one to the other should not be particularly hard.)

I mean, I have done a toy Forth implementation, but that is hardly more readable with no experience with the language.

One system that is almost conditions and restarts is 32-bit(!) Win32 SEH, but it is not particularly well-documented and the language bindings usually try rather hard to hide that (though, if you think about it, On Error Resume Next from classic VB is unimplementable on top of bare try/catch).

...

OK, you nerd-sniped me :) Here’s a toy (no subtyping! no introspection! no condition firewall[2]! no tracebacks! no support for native errors! etc.) condition system in Lua (sorry, nested functions in Python are painful):

  -- save as cond.lua
  
  local M = {}
  
  local error, unpack = error, unpack or table.unpack
  local running = coroutine.running
  local stderr = io.stderr
  local exit = os.exit
  local insert, remove = table.insert, table.remove
  
  -- conditions
  
  local handlers = setmetatable({}, {
      __mode = 'k', -- do not retain dead coroutines
      __index = function (self, key) -- no handlers by default
          self[key] = {}; return self[key]
      end,
  })
  
  local function removing(xs, x, ok, ...)
      assert(remove(xs) == x)
      if ok then return ... else error(...) end
  end
  
  -- establish a handler during call
  function M.hcall(h, f, ...)
      local hs = handlers[running()]
      insert(hs, h)
      return removing(hs, h, pcall(f, ...))
  end
  
  -- signal the given condition to currently active handlers
  function M.signal(...)
      local hs = handlers[running()]
      for i = #hs, 1, -1 do hs[i](...) end
  end
  local signal = M.signal
  
  function M.error(...)
      signal(...)
      stderr:write("error: " .. tostring(...) .. "\n")
      exit(1)
  end
  
  function M.warn(...)
      signal(...)
      stderr:write("warning: " .. tostring(...) .. "\n")
  end
  
  -- restarts
  
  -- invoke the given restart
  function M.restart(r, ...)
      local n = select('#', ...); r.n = n
      for i = 1, n do r[i] = select(i, ...) end
      error(r)
  end
  
  local function continue(r, ok, ...)
      if ok then return ok, ... end
      if ... == r then return false, unpack(r, 1, r.n) end
      error(...)
  end
  
  -- establish a restart during call
  function M.rcall(f, ...)
      local r = {}
      return continue(r, pcall(f, r, ...))
  end
  
  return M
Example: DOS-style abort-retry-ignore prompt implemented in the shell with some support in the (mock) I/O system and no support in the application:

  local cond = require 'cond'
  
  -- common condition types (XXX should use proper dynamic variables instead)
  
  local retry, use = nil, nil
  
  -- I/O library
  
  local function _gets()
      if math.random() < 0.5 then cond.error 'lossage' end
      return 'user input'
  end
  
  local function gets()
      local ok, value = cond.rcall(function (_use)
          use = _use
          local ok, value
          repeat ok, value = cond.rcall(function (_retry)
              retry = _retry
              return _gets()
          end) until ok
          return value
      end)
      -- ok or not, we got a value either way
      return value
  end
  
  -- application (knows nothing about errors)
  
  local function app()
      for i = 1, 5 do print(string.format("got: %q", gets())) end
      return "success"
  end
  
  -- shell
  
  local ok, value = cond.rcall(function (abort)
      return cond.hcall(function (err)
          io.stderr:write("I/O error: " .. err .. "\n")
          while true do
              io.stderr:write("[a]bort, [r]etry, [u]se value? ")
              local answer = io.read('*l')
              if answer == 'a' then cond.restart(abort, "aborted") end
              if answer == 'r' then cond.restart(retry) end
              if answer == 'u' then
                  io.stderr:write("value? ")
                  cond.restart(use, io.read('*l'))
              end
          end
      end, app)
  end)
  print(ok, value)
This is not a perfectly accurate semantic model for real condition system, but it should be enough to give a general idea of how these things work and what the advantage over bare unwinding mechanisms like try / throw or Lua’s pcall / error is.

[1] https://gigamonkeys.com/book/beyond-exception-handling-condi...

[2] https://www.nhplace.com/kent/Papers/Condition-Handling-2001....

Error handling is basically orthogonal to performance.

If a function call can fail, it should return an error, and that error should be managed by its caller. Any other approach means callstacks are unpredictable, which makes a program way way harder to model.

> that error should be managed by its caller

Objectively false. Correct answer: the error should be managed by the code that makes sense to handle the error.

> Any other approach means callstacks are unpredictable

Also false. I use conditions regularly, and my callstacks are very predictable - errors bubble upward through the call tree until they're handled. There's nothing simpler.

> which makes a program way way harder to model

Also false. I have no problem at all modeling and understanding my code rife with conditions.

You're wrong on every point. I don't know how to speak to you given the strength of your conviction, so shrug
I think the fact that you didn't actually refute any of my points speaks volumes about which of us is correct.
The problem is that a system of "conditions + restarts" approaches the generality of fully async code. Go can of course do async well enough via its goroutines.
For starters, there is no way to consume multiple return values of a function or method inline. This makes chaining extremely verbose and often results in having three lines of error handling per one line of "normal" logic.

Secondly, Go creates a silly dichotomy by introducing two completely different mechanisms for error processing: error values and panics. (Soon to be three, because people will start using generics.)

Thirdly, "errors are values" approach is extremely counterproductive when you have to create generic error handling (with logging, default behaviors on failur,e etc). Something as simple as printing why a web page panicked becomes an exercise in cleverness.

Go enthusiasts will probably say none of this matters if you follow some set of "good practices". However, even core language libraries often fail to handle errors consistently. (E.g. text/template.)

f(g()) does work if g returns multiple values and f takes that many args, but f(g(), h()) always requires that g and h each return a single value.
> Go error handling is shit.

See, if you actually think this, then there can be no common ground here. Just use other languages and stop complaining.