An error isn't just about what went wrong - it's also about what you were trying to do when it went wrong, so you can do something appropriate. If you handle exceptions several stack frames up, then you lose that information. Doing things the Go way also means you can make nice error messages that reflect the task, rather than a stack trace that only makes sense if you know the source code.
Yes, every intermediary must agree on the error type - it's the language-defined error type, which defines a single method, Error, which returns the error as a string. There's no need for any further agreement.
Functions encapsulate errors. If I call a function, it is entirely up to me how I wish to handle that error. "The code that finds an error" is the code that calls the function. All the context is local. There's no need to know how that function first encountered that error - that's part of the implementation detail of that function. At every stage, we make error handling decisions based on local context. This makes the code more maintainable, because there is genuine separation of concerns.
If you really want exceptions (a classic example is a recursive descent parser where you don't want to check every call), you can use panic and recover, making sure that callers will never see it - it's a local contract only.
How do you lose information with exceptions? You can handle an exception in the code immediately surrounding whatever detects the problem and your code semantics are no different than Go. The Go mechanism doesn't give the option of handling it several stack frames higher without having to implement handling in every single intermediary function.
The arguments I keep seeing for Go's semantics seem to the same as the ones about manual memory allocation - you must be in control every step of the way.
It looks like panic only takes a string so it isn't a good equivalent to exceptions. Whatever gets flung around should generally have enough information to make decisions and to generate meaningful error messages.
Yes, every intermediary must agree on the error type - it's the language-defined error type, which defines a single method, Error, which returns the error as a string. There's no need for any further agreement.
Functions encapsulate errors. If I call a function, it is entirely up to me how I wish to handle that error. "The code that finds an error" is the code that calls the function. All the context is local. There's no need to know how that function first encountered that error - that's part of the implementation detail of that function. At every stage, we make error handling decisions based on local context. This makes the code more maintainable, because there is genuine separation of concerns.
If you really want exceptions (a classic example is a recursive descent parser where you don't want to check every call), you can use panic and recover, making sure that callers will never see it - it's a local contract only.