Great tool, two big negatives. First, checking result values is dumb. It adds 50% more code. Add exception handling.
Second, the way imports work is obviously due to some internal google kitschy-ness. Remote import paths are so dumb. People set up entire domains and CDN's just to host some code. The import path has to have a specific format, you can't have three levels. github.com/me/sub1/module won't work, so everyone creates mypersonalgithub.com/sub1/module. It feels like one L3 at google set this up on a Friday, and the industry has to live with it. Of course all this code will be broken in 3 years.
There is no easy way to point imports to a local development copy of a repo because google uses a monorepo and everyone else has to live with it.
Your first negative is a matter of perspective, so depending upon your coding preferences you may be right.
For me personally, even with nearly 2 decades of C# (exceptions are used extensively) I prefer the Go way. Checking results values is not dumb, it's just not the way that suits you. And that's fine. Variety of preference is one of the reasons we have so many programming languages to choose from.
As for modules, despite extensive familiarity with packaging systems (nuget, npm, etc) I prefer the Go way. The import path is totally developer-defined (eg "github.com/russross/blackfriday/v2" which is three levels), and a single line in a Go module file redirects a repo import to your local development copy (by local folder location).
For me personally, with also nearly 2 decades of C# and C++, exceptions is the best possible way to handle errors there is, especially for large projects. I have worked on projects which were built on exceptions, and on projects which were built on return codes. Exceptions is hands down the cleanest. Instead of becoming a mess of call/error handle blocks, you code can clearly separate business logic and error handling. If I had 10 cents for every time I've fixed a bug when an error analysis is skipped because it is not supposed to happen until it is...
Golang way is terribly flawed. As a matter of fact I've watched a video yesterday on golang best practices, and got very excited. And then I realized, this is still a language where you can't throw an exception and that made me very sad, as I realized golang is going nowhere. It will be exciting for a few more years, like ruby was once, and then everybody realizes what a PITA it is for large codebases, and it shall become another ruby.
And for me having worked in both, errors as return values have been hands down better. The complexity added by an entirely different flow control has resulted in far more bugs for me to track down than anything else.
Similarly, it's for good reason that there's a whole other class of languages using things like the Result monad over throwing up all over everything.
That's not to say that there isn't better code written in exception based languages, but that your view is pretty myopic. The ergonomics are different in each approach, with different tradeoffs.
I'm pretty sure exceptions are better, for a couple of reasons.
The first is that the idea all Go programmers reliably propagate or wrap error codes, without losing important context, is not true. One of my first encounters with a serious Go codebase was at a consulting client, where I had a task to use their API. I sent it some input and got back a 500 Internal Server Error, no other info. OK, not ideal, but it was in development so I asked them to check the logs and find out what was going wrong. Guess what, the logs were useless. It logged at one or two places that an error had occurred, mostly close to the top level HTTP loop, but the actual location where the error was originated had been lost. Several layers in this app would convert error codes from one level of abstraction to another, also losing information. They shrugged, mystified. Just try things until you figure out what the issue is.
With exceptions this could not have happened. An exception has a stack trace. The developer needs do no work to get this valuable debugging aid, it's always there unless some bad code strips it somehow. Additionally, exceptions can wrap each other as causes, so code can work at high levels of abstraction whilst developers who are debugging can get precise error data from deep down the stack.
Another problem is the idea that Go developers never forget to propagate errors. Error handling in Go is tedious and there's no visible indication if you forget to do it or don't do it properly so sometimes it goes AWOL. The exceptional control flows still exist, just as they would if using exceptions, but now you have to write them manually instead of having the compiler write them for you.
A final problem is performance. Go has notoriously quite low performance, the people who say it's fast are usually comparing it to something like Python. Scattering hand written error handling code all over the place makes it harder for a compiler to move it out of the hot paths, because it's just a bunch of if statements. Exceptions by their nature tell the compiler that those error-handling edges won't execute very often, so they can be put out of the way in places that won't pollute the icache.
I agree with you on the stacktrace. The libraries that provide context via wrapping are good but woefully underused.
> Error handling in Go is tedious and there's no visible indication if you forget to do it or don't do it properly so sometimes it goes AWOL
This is also true, though I would expect teams to fail on linting so I don't share the concern. Is there an equivalent for unchecked exceptions (honest question)?
I'll take your word on the performance being better for exceptions, but I'm less convinced that Go's performance is a significant sticking point for the language.
A super important principal for large code base is locality. I should be able to look at a function and understand everything about its possible code paths. Exceptions make this impossible (most of my experience with exceptions coming from C++ and Python). People that think like me would forbid exceptions every where.
return values don't say anything about possible code paths. If you are saying the function should document what could go wrong when it is called, Java solved this 24 years ago when each method lists the exceptions they throw. If you need to know what the back trace/call stack was when the error happened, well that is stored in the exception.
People that use return codes simply don't check every possible value, otherwise they would see that it increases program length by at least 50% and makes the code unreadable. A common pattern in Go is this
if (err == MY_ERROR) {
log my error;
notify user;
return MY_ENCAPSULATING_ERROR_CODE
}
for high quality code this has to occur for every function call in the entire codebase. So you'll get huge functions which are mostly 'if' statements checking error codes, logging the results, and returning another error code that has to be checked again one step up the call stack.
moving from c++ to java 20 years ago it was clear that the c/c++ community didn't really know what they were doing in this regard. A lot of c/c++ dogma boilerplate code was holding it back. Too bad some of it made it into golang.
Explain the Java services that only ever return an HTTP 200 and either return the proper XML response or a java stack trace formatted into HTML.
And my point is those if statement are wanted by the people that write high reliability software in C. There just isn’t a substitute for thinking about errors. Even well done exception throwing systems will specific “this layer can throw, this middle layer won’t worry about errors, then this higher layer will restart or retry or whatever”. And in go within one layer you can usually have all those steps in one function and for error cases with the same handling just check for if err != nil. When I read code without explicit error handling I worry. I guess this debate is probably an aesthetic debate, and different type of code have different urgency. The extra check for error return is like the check for parameter correctness at the top of a function, which is common in robust C code. You can’t trust the callers too much and better to log an error than terminate the process.
> And my point is those if statement are wanted by the people that write high reliability software in C.
Because that's basically the only way of handling errors in C. That kind of software is written despite C's poor error handling, not because of it, and frankly, C is not the best example in writing high reliability software. Undefined behavior is a huge issue with it.
Second, the way imports work is obviously due to some internal google kitschy-ness. Remote import paths are so dumb. People set up entire domains and CDN's just to host some code. The import path has to have a specific format, you can't have three levels. github.com/me/sub1/module won't work, so everyone creates mypersonalgithub.com/sub1/module. It feels like one L3 at google set this up on a Friday, and the industry has to live with it. Of course all this code will be broken in 3 years.
There is no easy way to point imports to a local development copy of a repo because google uses a monorepo and everyone else has to live with it.