For example, consider:
foo, err := getFoo()
Now your scope has an "err" and you open yourself to logic errors such as: foo, err := getFoo()
... do stuff ...
if errr := makeBug(); errr != nil {
return err
}
Spot the (heh) error? It's caused by ":=" allowing variables to be reassigned.You can avoid this dangerous pollution, but it's super ugly:
var foo *Foo
if f, err := getFoo(); err == nil {
foo = f
} else {
return err
}
(You have to have the return in the second branch, otherwise "go vet" will complain. Sigh.)I have lots of these, and they're not helped by the fact that Go has no type-level mechanism to guard against the misuse of multiple-return values. That is, in the above, either "foo" or "err" are usually set, and if "err" is set, it means "foo" isn't. But the type system doesn't prevent you from accidentally ignoring "err" and using "foo", or vice versus. This gets even worse if the func returns a (non-pointer) struct as its value.
The fact that Go allows shadowing (exacerbated by the convention of using "err" as every single error variable) and promotes the reuse of variables (e.g. the above ":=", or declaring "err" outside a loop) also means it's super easy to assign the wrong thing.
All of the above are my single biggest source of bugs during development. That, and how accidentally using nil/zero values, including the damnable nil map/slice cases.
"go vet" (whose shadowing check is disabled by default (!)) and the various linters only go some ways towards helping here. The errcheck linter (which you can enable through gometalinter) is absolutely essential.
Most functions return an error so err is already defined:
func doIt(a, b) (result string, err error) { ... }
Then you are free to reuse err as needed. func doIt(a, b int) (result string, err error) {
var x, y string
x, err = doA(a)
if err != nil {
return
}
y, err = doB(b)
if err != nil {
return
}
result = x + y
return
} var foo *Foo
if foo, err := getFoo(); err == nil {
return nil, err
}
// foo is usable now ./test.go:14:15: foo declared and not used var foo *Foo
var err error
if foo, err = getFoo(); err != nil {
return nil, err
}
//foo is usable now foo, err := getFoo()
if err != nil {
return nil, err
}
"err" still ends up in the scope.I wonder what is the correlation between volume of complaining about minutia such as this compared to real life ability and level of accomplishment.
It's a decent language, but it's also a deeply flawed one. I could mention many other, bigger flaws, but this particular discussion was about errors.
Also, it's actually okay to debate minutia. Dismissing something as "not real problems" is, well, dismissive.
Not in scope. Because you are forced to deal with the error right here right now, sometimes it's in scope and sometimes, you're simply not able to do anything with the error so you effectively mishandle it.
Exceptions allow you to handle the error where it actually makes the most sense.
Anyway, Go-style error handling's lack of any of these issues is what makes it attractive to me: it's simple, straightforward and get's the job done. Granted, with a bit more typing and the occasional bug that could have been avoided by not having to type the (almost) same three lines once again, but not having to jump through any of the hoops I keep encountering in more sophisticated languages is so, so worth it to me. I feel like Go stays out of my way, lets me focus more on actually getting things done than on dealing with the language's intricacies.
This is a legitimate issue, although in Haskell you can use one error type like Go does if you want to.
> And as if that wasn't bad enough, there are also actual throw-catch-style exceptions, which are not checked in anyway
This is also the case in Go (panics and recover).
> I stopped writing Haskell maybe a year ago, but at least back then getting GHC to print a stack trace when an exception wasn't caught was a major feat that involved recompiling every single one of your hundreds of dependencies with profiling flags.
This is also the case in Go. Errors do not retain anything about their state. You need to use a package like https://github.com/go-errors/errors, which has the exact same issue you describe with dependencies.
It is considered good style to not let a panic escape your library API unless something just went very severely wrong that the library could not possibly continue to function normally.
>This is also the case in Go. Errors do not retain anything about their state.
Errors are an interface, you can put any state inside it you want and I've frequently done so (retain line counts in parser or network data for example)
SomeException is pretty much equivalent to Go's error, but it's not commonly returned by libraries and such so using it barely helps with the need to convert often. In addition all the types used by libraries as error values now need to have Exception implemented, which they don't. So, in practice, no, you cannot. Or rather, it wouldn't solve the core problem and create a new one.
>> And as if that wasn't bad enough, there are also actual throw/catch-style >> exceptions, which are not checked in anyway > > This is also the case in Go (panics and recover).
True, panic/recover exist, but in practice I find them to be more akin to setjmp/longjmp in C than throw/catch-style exceptions in other languages. Off the top of my head I can't recall a case where I had to recover from a panic that I didn't create in my own code (i.e. originated in a library call). Exceptions in Haskell and other languages on the other hand are usually all over the place (ok, usually not in pure code in Haskell) and I feel like I have to constantly keep them in mind to avoid letting one slip through the seams and then later having to track down where the hell that one came from.
>> I stopped writing Haskell maybe a year ago, but at least back then getting >> GHC to print a stack trace when an exception wasn't caught was a major feat >> that involved recompiling every single one of your hundreds of dependencies >> with profiling flags. > > This is also the case in Go. Errors do not retain anything about their state. > You need to use a package like https://github.com/go-errors/errors, which has > the exact same issue you describe with dependencies.
That's fair and indeed it would often be handy to know where an error came from. But since errors are just return values I don't really expect them to have a stack trace attached to them, just like I don't expect any other value to have that. It would still be neat for debugging sometimes, more so for errors than other values. I think there are some proposals for improving on this in Go 2. What really frustrated me about Haskell though was debugging throw/catch-style exceptions without a stack trace, because I basically had zero clues about where it came from and there were no clear paths to trace in the code. In Go on the other hand, errors don't just plop up from out of nowhere and panics, which can, print a nice stack trace when not recovered from.
So yes, it can be messy when using other people's code because this is very much an unstable part of the language right now. I still prefer that over the worst possible strategy (what Go picked) being effectively baked in.
Javascript messed it up a little bit with promises, because it's so easy to swallow errors. IMO, they should have made it so that unhandled promise rejections throw by default at the next turn of the event loop, with a method that could be called to opt out individual promises.
At least, V8 did IIRC. I use Creed[0] myself, which using it's `shim()` function gives you the same behaviour. It's quite lovely.
I don't think so. You can choose to ignore the error (I know it's bad practice), but it's allowed. And sometimes you don't know what to do with the error
I have said so in the intro textbook I wrote.
Even if the error can be ignored for fmt.Println, it is not clear from the example whether it was even taken into account or not. The following could be required:
_ := fmt.Println("hello world")
That means that the function call can return an error, and thus you have to use the return value, but the programmer decided to ignore it. Contrast it with: fmt.Println("hello world")
Did the programmer know there was possibly an error? What happens if there is an error (not specifically for this example, but in general), and you don't check for it? then it is ignored.In another language, the same line could be the source of an error; but only error handling depends on the programmer, not error checking. Moreover, the default handling strategy is to abort execution. Actually ignoring errors requires more work.
And errcheck is apparently no silver bullet: https://github.com/golang/go/issues/20148
Go allows you to do certain things but it's not a fallacy because it's something that's allowed but not necessarily tolerated by what most consider "good Go coding standards"
It strikes me a bit like saying "because most people don't wear seatbelts, cars are unsafe because you may get ejected out of the cockpit in a crash". Wear a seatbelt and use linters, I'm not going to excuse the malpractices of the larger industry.
Optional external tools, are exactly that, optional.
Lint was created for C in 1979, a minority used it or its commercial versions like Gimpel, until clang was introduced with static analysis built into the compiler.
Anything that isn't enforced by the language, tends to be ignored, hence why we need good defaults that make workarounds a big pain.
I'd rather just keep track of linter complaints.
> I'd rather just keep track of linter complaints.
Do you dislike "unused variables" being compilation errors, too?
It's more like every 5 lines. I love it as every error is handled correctly and in scope. No error is left behind. Most languages I've used devolve into the GLOBAL MASTER DEFAULT HANDLE ALL ERRORS which is just gross.