-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: require return values to be explicitly used or ignored (Go 2) #20803
Comments
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
While I certainly sympathize with the concerns here, I just want to state that 1) I think that wrapping expressions in That is, I'm on board with the problem, but not with the proposed solutions. |
ignore fmt.Println("Hello, world") is a bit clearer than ignore(fmt.Println("Hello, world")) But I agree that those are both valid concerns, and I'd be thrilled if we could find some way to address the underlying problems (missed values in general, and missed errors in particular) that resolves those concerns. |
I should note a point that I forgot to make in the original post (now edited to add): this isn't just a problem for errors, but also for functional-style APIs such as |
I know this has been discussed before, multiple times. The concern usually is: OMG, they did
What about maps? Is it okay to ignore the bool, or would I have to always add the Just how many programs would this proposal break? This seems like a Go 2.0 feature as it breaks the compatibility guarantee. I guess you could test that by writing a go vet function to do this and then run it first over the standard library, and then over random libraries from github or over the Go code internal at your company. |
Map lookups would still support both the one-value and two-value forms, but ignoring the result of a map lookup entirely would (continue to) be an error. These are ok. if m[x] { if _, ok := m[x]; ok { if y := m[x]; y { if y, _ := m[x]; y { // Verbose, but ok. This one is not ok, because the result is ignored entirely. The compiler already rejects it today (https://play.golang.org/p/0WMEDVzY44). m[x] |
Most of them. It is labeled "Go2" for good reason. :) |
Having lived through the (void)printf("hello, world\n"); era, I firmly believe some heuristics in vet are a better way to approach this problem. That's what #20148 is about. Like Ian, I appreciate the concern but dislike the proposed remedy. |
ISTM we don't really want to ignore errors; I think in most of these cases we really want to panic in case of error, because the programmer believes an error is not possible, or that a meaningful response to an error doesn't exist. So it seems like we want a generic
I guess we would also want |
So, what are the cases where it really is appropriate to ignore all of the return values of a function? These are always appropriate to ignore:
These are conditionally appropriate:
Unclear to me:
|
As you say, we have few cases where it is appropriate to ignore the return value, and we can do this clearly with an attribute. func Fprintln(w io.Writer, a ...interface{}) (n int, err error) optional |
Personally, I think that linters like https://github.com/kisielk/errcheck solve the problem quite nicely. |
IMHO this is not a problem with the language spec and can be remedied by linters. One of the reasons I stopped writing in Swift is because they kept introducing breaking changes with things like Please consider the awesome go community before changing something as fundamental as function returns. |
A generic "must" operator would not help for forgotten return-values from calls that do not return errors (such as |
Heuristic tools are for the author of the code, not the reader. I want to address both of those users, not just the author. If there is a consistent rule in the language itself, then the reader can easily see whether a return-value has been discarded, and can know that the author intentionally chose that behavior. With a heuristic tool, the reader can't distinguish between several possibilities:
Because the tool is not part of the language, its use discards important information which can only be recovered by ad-hoc documentation, and that documentation is (in my opinion) even more unpleasant than a leading fmt.Fprintln(w, stuff) // Ignore errors: w always wraps a bytes.Buffer. |
The other problem with heuristic tools is that they are unreliable for authors. If I run
We could bias toward over-reporting in order to reduce the occurrence of case (2), but that would make it much more likely that authors would simply turn off the check, further decreasing the confidence of readers of the code. We could provide a mechanism to suppress false-positives, but if we're going to do that, why not use it uniformly in the first place? Given that most of the ignorable return values I could find (in #20803 (comment)) are only conditionally ignorable, I'm skeptical that we can find a set of heuristics that are actually reliable in practice. |
I think that's a bad example: you probably want to check both, in case of delayed writes. (But I agree with your main point.) |
This is a really gross idea, but another option: With type aliases, the author of a package could add
and return it when it is always safe to ignore the return error like:
Since it's a type alias it only changes the spelling not the meaning. Linters could be aware of the idiom and whitelist anything that returned an alias spelled ignorableError to cut down on the noise. Readers aware of the idiom would know it's safe to ignore from the signature. It could be promoted from idiom to standard by making it a predeclared identifier. Of course if you change something so that the error is no longer ignorable and forget to update the signature . . . |
@jimmyfrasche I admire your creativity, but that wouldn't address non-error return types, and as far as I can tell wouldn't help the reader much for conditionally-ignorable return values. |
@bcmills I was thinking about this again today and I realized that a less gross variant of my last post would be a convention to use the blank identifier to name always-ignorable return params, like:
or
It's
† it's also easy to lint the case of you used to have an ignorable error but then you made a change and forgot to rename the return param. It wouldn't help a reader unfamiliar with the convention, but it's easy enough to pick up, especially if it's followed by the stdlib. I'm not sure there's a way to help with conditionally-ignorable errors. Really if you can only ignore an error in some conditions you:
|
Given the list in #20803 (comment), I think those are the only ones that really matter. Even the canonical example of an intentionally-ignored error, |
@bcmills Special cases for Let's say there was a way to document that you can sometimes ignore a result, for example an error, however. What does that tell you? That you can sometimes ignore the error. That's not very helpful unless you know when you can ignore the error. Short of a machine-checkable proof that a very sophisticated linter can use to analyze your program and verify that you can indeed safely ignore the error, the only recourse I see is to document the function with the cases when it is safe to ignore the error (and hope that if they change the docs get updated). A less sophisticated linter could see an annotation that a result is optional and that you didn't use it and assume you know what you're doing and not say anything, but that doesn't seem especially helpful. If there were a way to annotate that you must use a result, it's easy to detect when you didn't. But that's most things so it could infect the majority of func sigs. (Though coupled with the For the sake of argument say that 90% of results must be checked, 9% must be checked in certain scenarios, and 1% never need to be checked. Let's say we go with annotating the optionally and always ignorables because there are fewer of them (10% vs 90%). Then we can assume that anything that is not annotated must be checked. Then
The only gain I see is annotating the always ignorables since it at least can safely remove some noise from linter output. The |
I think there should be no ignorable errors, because they are really confusing from a standpoint of a plain logic. I think simple handling ALL return parameters is enough to eliminate typos. It's a balance between cognitive load and being error-prone. And if person explicitly ignores error - it's a mental problem, rather than the problem of exception handling. Not even mentioning the fact that if you introduce new stuff to Go - script-kiddies would use it everywhere. |
Considering Go refuses to compile programs with unused variables (https://golang.org/doc/faq#unused_variables_and_imports) I was surprised today when I found the code below. Since func normalizeChartName(chart string) string {
strings.Join(strings.Split(chart, "/"), "_") // This line :)
_, nc := path.Split(chart)
// We do not want to return the empty string or something else related to filesystem access
// Instead, return original string
if nc == "" || nc == "." || nc == ".." {
return chart
}
return nc
} @DeedleFake and @DmitriyMV: I am of course talking about unused function results, is my edited comment better understandable for you? |
Why would it fail to compile? Every variable is used. |
@cobexer I think you've meant "unused results" instead of "unused variable" since the line you provided wastes CPU\Memory but doesn't do anything to the app logic, because result of |
Proposal tweak: the family of |
I think, adding syntax to the function declaration for denoting pure functions could be used without breaking backwards compatibility. In this example, it is denoted by the exclamation mark following the function name. func returnTrue() bool {
return true
}
func returnFalse!() bool {
return false
}
func main() {
returnTrue() // no err
returnFalse!() // compile err
_ = returnFalse!() // no err
} |
@PlusMinus0 an important reason for fixing this issue is preventing inadvertent miss that a function returns an error. Eg a close() function, maybe you forget that it returns an error. The use of an extra symbol implies that you know (and remember!) that it is returning something. I think it should be the opposite: you would use an exclamation mark to indicate you are doing something potentionally dangerous, ie ignoring a return value. So if you don't have it and compiler errors out about an ignored return value, then you can add the exclamation mark and it's obvious to everyone that you 100% intended to ignore it. The compiler could just add a warning in this case. Of course this breaks existing behavior, eg fmt.Println(). So in addition, a marker on a function could be defined in the language, that enables the programmer to say whether it is safe to ignore its error return. Then the standard go lib would use that marker everywhere necessary, like fmt.Println(). |
I like this proposal, it's good to improve code quality. What about this syntax? //go:nodiscard
func New() *SomeType {
return &SomeType{}
} It can also apply to type: //go:nodiscard
type error interface {
Error() string
} Then any returned error can't be ignored implicitly. This approach is easy tooling and has very good back compatibility. I also like @PlusMinus0 's idea. |
Hi from the Java world -- I wanted to add some context, but I don't claim to know what things are like in your universe. I believe Swift has the opposite default: you mark only the methods whose results are safely ignorable, and Kotlin plans to attempt switching its default in-flight. We recently pulled off this in-flight switch for Google's entire internal Java codebase (asterisk) with very good results (many bugs found, few user complaints). That might not be feasible for you (dunno) but it has some advantages:
Maybe this is all unhelpful, but anyway we'd be glad to talk further about our Java/Kotlin experiences if it would help. |
Exactly. Ignorable result requires more thought: one should assume that there is a good reason the author makes the function return that info, so ignoring it ought to be done with care. So the calling code should make it clear to others that ignoring the return value has been done intentionally (and hopefully thoughtfully) rather than by mistake. I think it should be the caller that decides ; the author surely cannot foresee (except in rare occasions) whether a return value can never be ignored, could sometimes be ignored, or always be ignored. So the default should be: caller must use the return value, unless the caller has explicitly marked the value as ignored (it is the way of marking the return value in calling code that must maintain backward compatibility). |
Thanks @kevinb9n . That might have been a good choice initially, but unfortunately I don't see how we can make that change now. It would invalidate too much existing, working, Go code. |
Indeed I'll be quite interested to see how Kotlin pulls it off. An intermediate option -- again, I have no concept of how well-suited for you it is -- would be some way to let users opt in a whole directory/module/whatever of source files at once. That wouldn't have to break anyone, but users could still get the benefit of new code being "auto-opted-in". |
A "this value should not be discarded" would also be useful for slog.With("error", err) and subsequently logging nothing. We run into similar issues with |
ctx.ModuleErrorf for the main blueprint config object doesn't report an error, it just returns it. If go had something like C++'s `[[nodiscard]]` or rust's `#[must_use]` that would be useful here, but the issue has been open since 2017: golang/go#20803 Test: Presubmits Change-Id: I72709e6c5466d55f5c0f3fe51a25c29df0826271
Today, if a Go function returns both a value and an error, the user must either assign the error to a variable
or explicitly ignore it
However, errors that are the only return value (as from
io.Closer.Close
orproto.Unmarshal
) or paired with an often-unneeded return value (as fromio.Writer.Write
) are easy to accidentally forget, and not obvious in the code when forgotten.The same problem can occur even when errors are not involved, as in functional-style APIs:
For the few cases where the user really does intend to ignore the return-values, it's easy to make that explicit in the code:
And that transformation should be straightforward to apply to an existing code base (e.g. in a Go 1-to-2 conversion).
On the other hand, the consequences of a forgotten error-check or a dropped assignment can be quite severe (e.g., corrupted entries stored to a production database, silent failure to commit user data to long-term storage, crashes due to unvalidated user inputs).
Other modern languages with explicit error propagation avoid this problem by requiring (or allowing API authors to require) return-values to be used.
@discardableResult
attribute is set.@warn_unused_result
)#[must_use]
attribute.[[nodiscard]]
attribute, which standardizes the longstanding__attribute__((warn_unused_result))
GNU extension.ghc
Haskell compiler provides warning flags for unused results (-fwarn-unused-do-bind
and-fwarn-wrong-do-bind
).ignore
function in the standard library.I believe that the OCaml approach in particular would mesh well with the existing design of the Go language.
I propose that Go should reject unused return-values by default.
If we do so, we may also want to consider an
ignore
built-in or keyword to ignore any number of values:or
or
If we use a built-in function, we would probably want a corresponding
vet
check to avoid subtle eager-evaluation bugs:Related proposals
Extending
vet
checks for dropped errors: #19727, #20148Making the language more strict about unused variables in general: #20802
Changing error-propagation: #19991
The text was updated successfully, but these errors were encountered: