Golang's use of return values for error handling seems to have as many detractors as it does supporters. Since return values run counter to the [now] more common use of exceptions, most of what I've read seems to be written to defend Go's approach. My own view is that the approach is both good and bad.

I believe that the debate around Go's error handling is a symptom of the dissonance between why Go was written and how [some] people are using it (and it's hardly the only one!). I say this as someone who's used Go for both infrastructure programming as well as application (web) programming. (I think of infrastructure code as something akin to, but higher level than, system programming - but maybe I'm just making up terms).

When I'm doing lower-level programming, Go's error handling feels right. That's because errors are usually of immediate interest; the error is either actionable (and *must* be handled) or there's some important context that needs to be captured (local variables). These don't represent exceptional circumstances - the system can and should continue functioning - and errors as values results in both compact and readable code.

Where this falls apart is when the action to take is to fail, log details, and show a friendly error message. In this scenario, Go's error handling feels a bit like Java's checked exceptions. Every function call in every layer has to recognize the error and play its part in bubbling it up. It's a pain in the ass. It hurts readability, makes it a bit harder to refactor and might even result in worst stability - it's so easy to swallow those errors!

Consider deleting a row in a database. If you're building an application, a failure to delete is probably cause to show an error message and log / notify someone. If you're building code that proxies / balances X database, it's probably necessary to investigate the error in greater detail and take distinct action based on the context.

Back in 2003, Anders Hejlsberg talked about this and, in my opinion, nailed it. The overwhelming majority of exceptions cannot be intimately handled and should instead be handled by more centralized logic. At least, not in the types of apps typically written in Java or C# (or Ruby, or JavaScript, ...).

Again, I think it comes down to what you're writing. Given Go's genesis, return values feel like the better approach. But it's a pain for everyone else.