-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: improve error handing using guard
and must
keywords
#31442
Comments
I think that any error handling proposal should make it just as easy to annotate errors with additional information as it is to not annotate errors. That is true today, for example, where it is very easy to write |
A "default handler" for The Draft Design already has a default handler for Re Please add a link to this proposal on https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback |
I like the idea of using |
I ran some quick numbers on the kubernetes code base (per April 15 2019) to get some idea on actual error handling usage in production quality code. # number of 'if err != nil' (and friends):
rgrep --include='*.go' -e 'if err.*!= nil' . | grep -v generate | wc -l
58074
# number of bare error returns (no additional error handling, no error annotations)
rgrep --include='*.go' -B1 -e 'return.*err$' . | grep 'if err' | grep -v generate | wc -l
30167
# number of annotated error returns
rgrep --include='*.go' -B1 -e 'return.*fmt.Errorf(.*err.*' . | grep 'if err' | grep -v generate | wc -l
2843
# number of unique error annotation messages
rgrep --include='*.go' -e 'return.*fmt.Errorf(.*err.*' . | grep -v generate | grep -o '".*"' | sort | uniq | wc -l
2517 # number of panic(err)s
rgrep --include='*.go' -e 'panic(err)' . | grep -v generate | wc -l
702
# number of 'defer *.Close()'
rgrep --include='*.go' -e 'defer' . | grep -v generate | grep Close | wc -l
3286 This last number is interesting: there are almost 3300 errors ignored in Kubernetes (including all dependencies). These are potential bugs. Note that it only counts unchecked Close() functions and assumes all the Close methods return an error. These lines would really benefit from a 'must' or 'guard' (or check) keyword to make actually handling and not ignoring theses errors more easy. If think this is something that deserves more attention. Notes:
|
@ianlancetaylor I agree, and my argument is to keep using the |
@markusheukelom I've got a few questions regarding "defer must w.Close()" in your initial example; I'm just trying to understand your proposal better. A defer statement requires a function invocation as operand, so that's not technically the case here. But let's assume the "must w.Close()" is accepted with the "defer" and that the entire stanza "must w.Close()" is run as the deferred function. So if the w.Close() reports an error, this would panic when the defer is run at the end. But if I wanted to return the w.Close() error, presumably I would want to write "defer guard w.Close()", wouldn't I? That way, if the function executes properly but for the w.Close(), I'd get the error from w.Close(). Is that your intention? And finally, if we already have an error that's being returned, and w.Close() returns another error, wouldn't that error clobber (overwrite) the prior error being returned? Or should "defer guard" be disallowed? |
Yes, you've understood it as I intented it to work. And yes
It would be a little weird if the last line would not compile, as it does not do something fundamentally different than the line before it. I do agree with you that Thinking about this however did make me realise that an argument can be made to have
|
If a deferred function fails, you almost certainly need local code to handle that failure, e.g. retry or log something and abort the current context. Unless the function can't return an error any other way, However, a Go 2 error handling scheme is likely to provide error handlers, so could potentially offer deferred handlers:
|
@markusheukelom Thanks for your answers. I don't see convincing reasons for allowing something like defer func(f *os.File) {
must f.Close()
}(r) so there's no need for |
Here is an idea about The basic syntax is similar to 1. If you introduce a variable in an
As a ternary initializer:
2. For expressions returning a final error value
|
@carlmjohnson It seems to me that your proposed guard val, ok := myMap[key]; ok {
// handle !ok here
}
// val is in scope here can be written today as val, ok := myMap[key]
if !ok {
// handle !ok here
}
// val is in scope here And if you don't want the Your ternary initializer guard name := obj.Name(); name != "" {
name = "Unknown"
}
// name is in scope here becomes name := obj.Name()
if name == "" {
name = "Unknown"
}
// name is in scope here And finally guard f := os.Open(file)
defer f.Close() becomes f := try(os.Open(file))
defer f.Close() It is hard to see why this should be any better than writing an In short, I see no benefit in making |
That makes sense. I think an unless-like In general, all of the handler block proposals seem to suffer from not being better than an if statement. |
That is exactly one of our observations as well. Only if the error handling is the same in many places (say where the error is decorated the same way), does it make sense to share and factor out that code, in some sort of handler. It appears that those cases are not the common. |
The Error Values feature enabling returned errors to be wrapped should make those cases a lot more common. However it may take a couple years after the arrival of 1.13 to see that impact. And there is one proposal which is better than a vanilla |
@markusheukelom thank you for realizing that errors in |
This is good, but it's not interesting if we want to wrap the error |
2 new keywords are introduced, with "complex" descriptions. However, it can be argued that these improve readability and the verbosity of error handling, as per the original error-handling draft design. Each keywords treats errors as special values. It doesn't allow wrapping. Based on historical decisions with regards to error-handling, this proposal is bound to fail. However, it's backwards compatible. |
Hi,
for example ( in file exception.go ): package main
// sample error
type sizeError struct {
Err error
}
func errorInit[T any](d T, err error) error {
switch v := err.(type) {
// for close some file with error
case *fs.PathError:
if f, ok := any(d).(*os.File); ok {
f.Close()
}
panic(v)
// sometime one error not necessary
case sizeError:
// do something
return nil
// for sometimes we get one error but needed send custom error
case Error404:
// do something
return d.JSON(404, gin.H{
"message": "not found!",
})
// for sometime we just now no have exception
default:
panic(v)
}
}
// if we use errorInit not needed f.Close()
func Sample(name string) error{
// send tow data on os.File and error
// if program get error
// guard get error from errorInit
// and return this func error
// else f value get os.File
f := guard os.Open(name)
guard f.Write([]byte("FOO"))
//throw sizeError
s := guard getSize(name)
fmt.Println(s)
return nil
} If not found in errorInit program run default (guard) and return only error without custom proses. |
What's the status of this proposal? Seems like there hasn't been much activity for a while. I personally like it and am interested in where it's at! |
At the moment we are not focussing on error handling through language changes; no progress has been made here at this point. |
guard
and must
keywordsguard
and must
keywords
I'll start with an example. Here's a version of the
CopyFile
example function form the proposal for improved error handling in Go 2.0, rewritting usingmust
andguard
:The
must
keyword is syntactic sugar topanic
on any error returned by a function call:is conceptually equivalent to
The
must
keyword can only be used on function calls that return an error as last return argument. In functions that do not return an error as last return value, or where return values are unnamed,must
is exactly equivalent to the code above. In case a function does return a named error value, when a error is returned from the call,must
assigns this error to function's named return error value, then executes alldefer
red functions just like a normal panic, and then uses the return error value when propagating the panic upwards. As a result, adefer
statement can therefore also be used to augment errors raised in apanic
caused by themust
keyword.The
guard
keyword is syntactic sugar to return from a function when a function call returns in an error (as last argument):is equivalent to:
The
guard
keyword can be used only on function calls that return anerror
as last return value, in functions that also return anerror
as last value.The
must
andguard
keywords cannot be used in conjunction with thego
keyword.Benefits
The
if err := ...; err != nil {}
pattern has several drawbacks:noise
in the codedoFileStuff(os.Open("foobar.txt"))
does not work, becauseOpen
also returns an (optional) errorBoth of these drawbacks would disappear (in second case either
guard
ormust
can be used). Of course there is the very valid argument that errors should be augmented with more relevant error information that only the caller can provide. In general there are three situations:In the first case we can just use
guard
(ormust
) and we're done. In the second case, thedefer
technique can be used to add function specific information (mostly the call argument values) for all errors that are returned (includingpanic
s viamust
).Finally there is the case where we want to add information to a specific error only. In that case, as well in the case more has to be done than just augmenting the error, the current
if err != nil {}
pattern should be used: especially if more error handling code is needed there is no real reason to move this important code elsewhere as it is specific to the code directly above it.--
As an extra benefit, with the
must
keyword all functions of theMustXXX()
pattern are no longer needed.Benefits over the
2.0
proposalMy main concern with the
handle
keyword is that it adds another code path: the error handling path. While it would be fine for error augmentation (added relative information to the error), you need to 'read down, then read up' checking allhandle
clauses to see what happens on an error. This not ideal. If any additional stuff must happen that should really be done under that function call, using theif err != nil {}
: it is there where the error happens and any error handling code should be below it, not somewhere above in any of multiple of levels.The advange of
must
andguard
, in my opinion, overhandle
andcheck
are that specific error handling code stay where it is done now, but all other cases, where we need to add general function wide info, or no info at all, can be handle much more easily and cleaner.The text was updated successfully, but these errors were encountered: