go: proposal: spec: require return values to be explicitly used or ignored (Go 2)
Today, if a Go function returns both a value and an error, the user must either assign the error to a variable
v, err := computeTheThing()
or explicitly ignore it
v, _ := computeTheThing()
However, errors that are the only return value (as from io.Closer.Close or proto.Unmarshal) or paired with an often-unneeded return value (as from io.Writer.Write) are easy to accidentally forget, and not obvious in the code when forgotten.
tx.Commit() // Store the transaction in the database!
The same problem can occur even when errors are not involved, as in functional-style APIs:
t := time.Now()
t.Add(10 * time.Second) // Should be t = t.Add(…)
strconv.AppendQuote(dst, "suffix") // Should be dst = strconv.AppendQuote(…)
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:
_, _ = fmt.Fprintln(&buf, v) // Writes to a bytes.Buffer cannot fail.
go func() { _, _ = fmt.Fprintln(os.Stderr, findTheBug()) }()
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.
- Swift warns about unused return values, but allows the warning to be suppressed if the
@discardableResultattribute is set.- Prior to a change in Swift 3, return-values could be ignored by default (but a warning could be added with
@warn_unused_result)
- Prior to a change in Swift 3, return-values could be ignored by default (but a warning could be added with
- Rust has the
#[must_use]attribute. - C++17 has the
[[nodiscard]]attribute, which standardizes the longstanding__attribute__((warn_unused_result))GNU extension. - The
ghcHaskell compiler provides warning flags for unused results (-fwarn-unused-do-bindand-fwarn-wrong-do-bind). - OCaml warns about unused return values by default. It provides an
ignorefunction 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:
go func() { ignore(fmt.Fprintln(os.Stderr, findTheBug())) }()
or
go func() { ignore fmt.Fprintln(os.Stderr, findTheBug()) }()
or
go func() { _ fmt.Fprintln(os.Stderr, findTheBug()) }()
If we use a built-in function, we would probably want a corresponding vet check to avoid subtle eager-evaluation bugs:
go ignore(fmt.Fprintln(os.Stderr, findTheBug())) // BUG: evaluated immediately
Related proposals
Extending vet checks for dropped errors: #19727, #20148
Making the language more strict about unused variables in general: #20802
Changing error-propagation: #19991
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 130
- Comments: 41 (24 by maintainers)
While I certainly sympathize with the concerns here, I just want to state that 1) I think that wrapping expressions in
ignoremakes code harder to read by burying the lede; 2) in a language that values simplicity having the canonical hello, world example start with_, _ =seems to me to be unfortunate.That is, I’m on board with the problem, but not with the proposed solutions.
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 https://github.com/golang/go/issues/20148 is about.
Like Ian, I appreciate the concern but dislike the proposed remedy.
ignoreas a keyword rather than a builtin might address the lede-burying problem somewhat. IMO,is a bit clearer than
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.
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
@discardableResult. It didn’t fix a problem with the language but broke a lot of things. First I gave up on using third party libs (because most don’t have cross version support) then I gave up on Swift entirely.Please consider the awesome go community before changing something as fundamental as function returns.
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
Must. This might seem weird, but what if the exclamation point (!) could be used as a postfix operator to conditionally panic. If the value is abool, panic if it’s nottrue, and if it’s anerror, panic if it’s notnil. It could also be used with multiple return values, only looking at the last value. Since it’s postfix it avoids burying the lede, but indicates the programmer understands they’re ignoring an error value. (!) should always consume one or more values but never return anything, and should fail to compile if the type of the last value passed in is notboolorerror.I guess we would also want
go f(x)!anddefer f(x)!to be equivalent togo func(z T) { f(z)! }(x)anddefer func(z T) { f(z)! }(x).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.
@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
strings.Joinis ignored.@nerdatmath
A generic “must” operator would not help for forgotten return-values from calls that do not return errors (such as
(time.Time).Add). Furthermore, in many cases we really do intend to ignore the error, not not panic if it occurs: for example, if we are already returning a non-nilerrorfrom writing a file, we typically do not care whether itsClosemethod returns an error.A “this value should not be discarded” would also be useful for
slog.Logger.With()to prevent users from writingand subsequently logging nothing.
We run into similar issues with
logrus.WithError(err)today.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
strings.Split()andstrings.Join()are pure functions (https://en.wikipedia.org/wiki/Pure_function) I expect the Go compiler to refuse compiling the code below.(from https://github.com/argoproj/argo-cd/blob/24526ded21049496fea5c61bf187ddc3ca2881e3/util/helm/client.go#L321)
@DeedleFake and @DmitriyMV: I am of course talking about unused function results, is my edited comment better understandable for you?
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 . . .
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
_, _ =orignore:Most of them. It is labeled “Go2” for good reason. 😃
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.
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.
Proposal tweak: the family of
fmt.Printlnfunctions should return nothing. Folks that want to check thefmt.Printlnerror should usefmt.PrintlnVor a more appropriate name. That way you get the nice hello world, and error checking isn’t delegated to a linter that often isn’t there. Upgrading existing code is as easy as a string replace. We can leave this painful language wart to the history books.Why would it fail to compile? Every variable is used.
If you want to talk logic, there is a logical flaw in that argument. You are conflating implementing a method in order to conform to an interface, with invoking that method through an interface type. For example, I will never need to check the error from Write in
@bcmills Special cases for
bytes.Bufferin the standard library are easy enough to special case in a linting tool but that doesn’t help with the problem of making it clear to someone looking at the docs that it can always be ignored or the problem of discovering such funcs outside the standard library. A convention here would be useful to people and linters.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
_convention for always ignorables it would give you the set of optionally ignorables as a by-product).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
_ = f()notation let’s you be explicit at the call site as a signal to readers and linters alike that you’re ignoring a result.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
time.Time.Addorstrconv.Append*. The missed return value is sometimes the only clue the reader has as to whether an API is functional or mutative.