I love multiple returns/errors. I find that I never mistakenly forget to handle an error when the program won't compile because I forgot about the second return value.
I don't use go at work though, I use a language with lots of throw'ing exceptions, and I regularly miss handling exceptions that are hidden in dependencies. This isn't the end of the world in our case, but I prefer to be more explicit.
golang is not a modern language (how old it is is irrelevent), and the people who designed it did not have a proper language design background (their other accomplishments are a different matter).
Having worked on larger golang code bases, and I've seen several times where errors are either ignored or overwritten accidentally. It's just bad language design.
> people who designed it did not have a proper language design background
Irrelevant.
> It's just bad language design.
try { ... } catch(Exception ex) { ... }
The error here is explicitly handled, and cannot be accidentally ignored. Unlike golang where it's quite easy for errors to go ignored accidentally.
Each language has its wonks.
It's not similar to that at all. Without it, the exception bubbles up until it gets caught somewhere, or crashes the program with a useful stacktrace.
With golang, it just goes undetected, and the code keeps running with corrupt state, without anyone knowing any better.
If you care with go, use errcheck.
class Query {
class QueryResult {
Exception error;
Value result;
}
public QueryResult query() {
try {
return doThing();
} catch(Exception e){
return new QueryResult(error, null);
}
}
}
Gorm is going out of its way to make error handling suck.It's about the default error handling method being sane. In exception based languages, an unhandled error bubbles up until it reaches a handler, or it crashes the program with a stacktrace.
Compare to what golang does, it's somewhat easy to accidentally ignore or overwrite errors. This leads to silent corruption of state, much worse than crashing the program outright.
Gorm isn't using the default error handling.
I've seen a bunch of code that does the equivalent of the Java I posted above. Mostly when sending errors across the network.
fmt.Println("foo")
Compare to a language with exception handling where an exception will get thrown and bubbles up the stack until it either hits a handler, or crashes the program with a stack trace.And I was referring to accidental ignoring. I've seen variations of the following several times now:
res, err := foo("foo")
if err != nil { ... }
if res != nil { ... }
res, err = foo("bar")
if res != nil { ... }>The following line in golang ignores the error:
fmt.Println("foo")
fmt.Println() is blacklisted for obvious reasons, but this: a := func() error {
return nil
}
a()
results in: go-lint: Error return value of 'a' is not checked (errcheck)
>And I was referring to accidental ignoring. I've seen variations of the following several times now: res, err := foo("foo")
if err != nil { ... }
if res != nil { ... }
res, err = foo("bar")
if res != nil { ... }
results in: go-lint: ineffectual assignment to 'err' (ineffassign)That's the issue with the language, there are so many special cases for convenience sake, not for correctness sake. It's obvious why it's excluded, but it doesn't make it correct. Do you want critical software written in such a language?
Furthermore, does that linter work with something like gorm (https://gorm.io/) and its way of handling errors? It's extremely easy to mis-handle errors with it. It's even a widely used library.
Perhaps more fundamental: it’s impossible to accidentally use an uninitialized “success” return value when the function actually failed, which is easy to do in C, C++, Go, etc.
func foo() (*SomeType, error) {
...
return someErr
}
...
result, err := foo()
if err != nil {
// handle err
}
// handle result
vs type Result struct {
Err error
Data SomeType
}
func (r *Result) HasError() bool {
return r.Err != nil
}
func bar() *Result {
...
return &Result { ... }
}
...
result := bar()
if result.HasError() {
// handle result.Err
}
// handle result
I'm not really sure I see the benefit to the latter. In a language with special operators and built-in types it may be easier (e.g. foo()?.bar()?.commit()), but without these language features I don't see how the Result<T> approach is better.The Result<T> approach prevents you from accessing Data if you haven't handled the error, and it does so with a compile-time error.
Even with Go's draconian unused variable rules, I and my colleagues have been burned more than once by forgotten error checks.
https://github.com/kisielk/errcheck
https://golangci-lint.run/usage/linters/ has a solid set of options.
That's the point! I want language features!
I don't want to wait 6 years for the designers to bake some new operator into the language. I want rich enough expression so that if '?.' is missing I just throw it in as a one-liner.
Generics is one such source of richness.
At $work we definitely have examples where we care about preserving errors, and if that tool were implemented in Go a solution like a Result struct containing an error instance and a data type instance could make sense.