Skip to content
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: reduce noise in return statements that contain mostly zero values #21182

Open
jimmyfrasche opened this issue Jul 26, 2017 · 74 comments
Labels
dotdotdot ... error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@jimmyfrasche
Copy link
Member

jimmyfrasche commented Jul 26, 2017

Update: the current proposal is to permit return ..., v to return the zero value for all but the last result, and to return v as the last result. The most common use will be return ..., err.

A variant under discussion include return ..., v1, v2 to return zeroes for all but the last N.

Another variant is to permit return ... to return zeroes for all.

In general ... is only permitted if it omits one or more values--func F() err { return ..., errors.New("") } is not permitted.


Proposal

In return statements, allow ... to signify, roughly, "and everything else is the zero value". It can replace one or more zero values.

This is best described by example:

Given the function signature func() (int, string, *T, Struct, error):

return 0, "", nil, Struct{}, nil may be written return ...

return 0, "", nil, Struct{}, err may be written return ..., err

return 0, "", nil, Struct{X: Y}, nil may be written return ..., Struct{X: Y}, nil

return 1, "", nil, Struct{}, nil may be written return 1, ...

return 1, "a", nil, Struct{}, nil may be written return 1, "a", ...

return 1, "", nil, Struct{}, err may be written return 1, ..., err

return 1, "a", nil, Struct{X: Y}, err may be written return 1, "a", ..., Struct{X: Y}, err

The following is invalid:

return ..., Struct{X: Y}, ... — there can be at most one ... in a return statement

Rationale

It is common for a function with multiple return values to return only one non-zero result when returning early due to errors.

This creates several annoyances of varying degrees.

When writing the code one or more zero values must be manually specified. This is at best a minor annoyance and not worth a language change.

Editing the code after changing the type of, removing one of, or adding another return value is quite annoying but the compiler is fast enough and helpful enough to largely mitigate this.

For both of the above external tooling can help: https://github.com/sqs/goreturns

However, the unsolved problem and motivation for the proposal is that it is quite annoying to read code like this. When reading return 0, "", nil, Struct{}, err unnecessary time is spent pattern matching the majority of the return values with the various zero value forms. The only signal, err, is pushed off to the side. The same intent is coded more explicitly and more directly with return ..., err. Additionally, the previous two minor annoyances go away with this more explicit form.

History

This is a generalized version of a suggestion made by @nigeltao in #19642 (comment) where #19642 was a proposal to allow a single token, _, to be sugar for the zero value of any type.

I revived the notion in #21161 (comment) where #21161 is the currently latest proposal to simplify the if err != nil { return err } boilerplate.

Discussion

This can be handled entirely with the naked return, but that has greater readability issues, can lead too easily to returning the wrong or partially constructed values, and is generally (and correctly) frowned upon in all but the simplest of cases.

Having a universal zero value, like _ reduces the need to recognize individual entries as a zero value greatly improving the readability, but is still somewhat noisy as it must encode n zero values in the common case of return _, _, _, _, err. It is a more general proposal but, outside of returns, the use cases for a universal zero value largely only help with the case of a non-pointer struct literal. I believe the correct way to deal that is to increase the contexts in which the type of a struct literal may be elided as described in #12854

In #19642 (comment) @rogpeppe suggested the following workaround:

func f() (int, string, *T, Struct, error) {
  fail := func(err error) (int, string, *T, Struct, error) {
    return 0, "", nil, Struct{}, err
  }
  // ...
  if err != nil {
    return fail(err)
  }
  // ...
}

This has the benefit of introducing nothing new to the language. It reduces the annoyances caused by writing and editing the return values by creating a single place to write/edit the return values. It helps a lot with the reading but still has some boilerplate to read and take in. However, this pattern could be sufficient.

This proposal would complicate the grammar for the return statement and hence the go/ast package so it is not backwards compatible in the strict Go 1 sense, but as the construction is currently illegal and undefined it is compatible in the the Go 2 sense.

Possible restrictions

Only allow a single value other than ....

Only allow it on the left (return ..., err).

Do not allow it in the middle (return 1, ..., err).

While these restrictions are likely how it would be used in practice anyway, I don't see a need for the limitations. If it proves troublesome in practice it could be flagged by a linter.

Possible generalizations

Allow it to replace zero or more values making the below legal:

func f() error {
  // ...
  if err != nil {
    return ..., err
  }
  // ...
}

This would allow easier writing and editing but hurt the readability by implying that there were other possible returns. It would also make this non-sequitur legal: func noop() { return ... }. It does not seem worth it.

Allow ... in assignments. This would allow resetting of many variables to zero at once like a, b, c = ... (but not var a, b, c = ... or a, b, c := ... as their is no type to deduce) . In this case I believe the explicitness of the zero values is more a boon than an impediment. This is also far less common in actual code than a return returning multiple zero values.

@gopherbot gopherbot added this to the Proposal milestone Jul 26, 2017
@ianlancetaylor ianlancetaylor added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Jul 26, 2017
@OneOfOne
Copy link
Contributor

Why not named returns? func f() (i int, ss string, t *T, s Struct, err error) {}

@rogpeppe
Copy link
Contributor

I like this idea, but I can't think of any place where I'd use it for anything other than filling in all but the last value. Given that, I think one could reasonably make it a little less general and allow only this form (allowing any expression instead of err, naturally)

return ...err

Note the lack of comma. I'm not sure whether it's better with a space before "err" or not.

@jimmyfrasche
Copy link
Member Author

@OneOfOne Naked returns are fine for very short functions but are harder to scale: it gets too easy to accidentally return partially constructed values or the wrong err because of shadowing. Other than that, or maybe because of that, I like the explicit syntax better. A naked return says "go up to the function signature to see what can be returned, then trace through the code to see what actually gets returned here" whereas return ..., err says "I'm returning a bunch of zero values and the value bound to the err in scope"

@rogpeppe that was the original syntax proposed that I based this proposal off of. I don't like it because it appears to be a spread operator common in dynamic languages so it's a bit confusing. Having the comma makes it superficially more similar to "⋯" in mathematical writing and with like purpose. I agree that this would almost always be used as return ..., last where last is a bool or error and sometimes as a return ..., but I don't see any particular reason to artificially limit it to that. If it's not very useful it will not get used often. Is there any particular concern that a line like return 7, ... would be more confusing or error prone than a return ..., true?

@rogpeppe
Copy link
Contributor

@jimmyfrasche I just don't see that it would ever be used, and given that, the comma seems like unnecessary overhead for what would be a very commonly used idiom.

How many places in existing code can you find where eliding all but the first argument (or all but several arguments) would be useful?

@ianlancetaylor
Copy link
Member

If we permit both ..., v and v, ... how do we justify the exclusion of ..., v, ...? Except for that fact that it is impossible to implement?

@jimmyfrasche
Copy link
Member Author

@rogpeppe return ...aSlice looks too much like https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator That's not a strong argument against it, but I'd still rather not see it. While I would be surprised if this proposal is accepted in any form, I certainly wouldn't be mad if that variant was the one accepted, as it is the most common case. I really don't see any justification for the limitation, personally. It would be interesting to run an analysis on go-corpus to see if there are any. I don't have the time to do that for a bit, so anyone can feel free to beat me to it.

@ianlancetaylor that's a fine point. Impossible to implement is justification enough for me. Though it would be possible to implement in some cases, where v is of a type returned only once, but then subtle and distance changes could make it suddenly ambiguous. I would note that the other two existing uses of ... are also "at most once". You can't do func(a ...string, b ...int) or append(bs, s1..., s2...) even though the first is only sometimes unambiguous and the latter would be merely inefficient (or rather non-obviously inefficient).

Another option, that I'm fairly sure is a bad idea, would be to allow "keyed returns" to work in conjunction with named returns, by analogy with keyed struct literals:

func() (a int, b string, err error) {
  // ...
  if err != nil {
    return err: err
  }
  // ...
  return b: name, a: -x // I purposely flipped the order here
}

though that would interact poorly with the semantics of the naked return. If a is non-zero what happens when I write return err: err? The answer should clearly be that only err is returned, but it's still a potential source of confusion.

@mewmew
Copy link
Contributor

mewmew commented Aug 12, 2017

Counter-proposal that has been suggested elsewhere in the past (#19642). Allow _ to be used as the zero value of any type in expressions. Currently _ in var _ T = foo may be thought of as foo > /dev/null (from Effective Go). Similarly, _ in var foo T = _ could be thought of as foo < /dev/null. Then _ could be used in return statements.

E.g. given the function signature func() (int, string, *T, Struct, error):

return 0, "", nil, Struct{}, nil may be written return _, _, _, _, _

return 0, "", nil, Struct{}, err may be written return return _, _, _, _, err

return 0, "", nil, Struct{X: Y}, nil may be written return _, _, _, Struct{X: Y}, _

return 1, "", nil, Struct{}, nil may be written return 1, _, _, _, _

return 1, "a", nil, Struct{}, nil may be written return 1, "a", _, _, _

return 1, "", nil, Struct{}, err may be written return 1, _, _, _, err

return 1, "a", nil, Struct{X: Y}, err may be written return 1, "a", _, Struct{X: Y}, err

@davecheney
Copy link
Contributor

When your functions has so many return values that typing them becomes a chore, that's a sign that you need to redesign your function, not the language.

@mewmew
Copy link
Contributor

mewmew commented Aug 12, 2017

When your functions has so many return values that typing them becomes a chore, that's a sign that you need to redesign your function, not the language.

I think the example was given simply to provide a one of each function signature that is useful as a showcase. While not likely to see so many return values in real world code, returning the zero value of a struct using _ rather than image.Rectangle{} may improve readability; at least that's the idea of the proposal.

@jimmyfrasche
Copy link
Member Author

@davecheney indeed.

My argument is that the primary benefit of the succinct syntax is that it improves the readability. If it makes it easier to type that's just a bonus.

If you see return ..., err you don't need to think about what else is being returned.

You don't need to double check for things that are suspiciously close to a zero value like return Struct{''}, err or return O, err or return nil, err (when nil has been shadowed for some reason).

It's immediately obvious that the only relevant value is err. Idioms such as

if err != nil {
  return ..., err
}

can be pattern matched by your brain as a unit without having to actually inspect anything. I'm sure we all do that now with similar blocks that contain one or more zero value-like expression. It's bitten me once or twice when I was debugging and my eye glazed over something that looked too close to a zero value making it hard to spot the obvious problem (I of course do not admit publicly to being the person who shadowed nil . . .).

I'm fine with how it is, however. This is just a potential way to make it a little bit easier.

@mewmew yes this proposal is based on a comment from that proposal (see the History section). I don't particularly see the point of the generic zero value except in the case of returns. It would solve the same problem, of course.

(I would like to be able to use {} as the zero value of any struct when its type can be deduced from the rest of the statement.)

@nigeltao
Copy link
Contributor

@davecheney sometimes it's not the number of return values, it's their struct-ness. Typing return image.Rectangle{}, err can be a chore. The image.Rectangle{} is the longest but also least important part of the line.

That said, this particular proposal is not the only way to sooth that chore, as the OP noted.

@ianlancetaylor
Copy link
Member

Reopening per discussion in #33152.

@ianlancetaylor ianlancetaylor reopened this Oct 9, 2019
@golang golang unlocked this conversation Oct 9, 2019
@earthboundkid
Copy link
Contributor

I am in favor of allowing only the return ..., x form. If other forms are good, they could be added later.

The main benefit of this for me is that it would let me use a dumb macro to expand ife into

if err != nil {
   return ..., err
}

Yes, a sufficiently smart IDE macro could look at the function return arguments to fill those in for me, but why not just simplify it so only the important information is emphasized?

@bradfitz
Copy link
Contributor

bradfitz commented Oct 15, 2019

What about letting return take 1 thing regardless of how many results the func has, as long as the assignment is unambiguous to exactly 1 of the return types?

So:

    func foo() (*T, error) {
        if fail {
             return errors.New("foo")
        }
        return new(T)       
    }

@ianlancetaylor
Copy link
Member

Does anybody see any problems with this language change?

We think we should consider just the simple case: return ..., val1, val2 where val1 and val2 become the final results of the function. Typically, of course, this would be just return ..., err. The other cases don't seem to arise enough to worry about. This would only be permitted if there are other results; it could not be used in a function that returns only error. The omitted results would be set to the zero value, even if the result parameters are named and currently set to some non-zero value.

-- for @golang/proposal-review

@jimmyfrasche
Copy link
Member Author

@bradfitz that seems like it could cause too much fun when the return signature changes in a long func or one of the return types now satisfies an interface. It would also be unusable with interface{}, though that's a bit niche.

@bradfitz
Copy link
Contributor

It would also be unusable with interface{}, though that's a bit niche.

That's a feature.

@rsc
Copy link
Contributor

rsc commented Dec 21, 2022

Placing on hold for more thought and later discussion.

@rsc
Copy link
Contributor

rsc commented Dec 21, 2022

Placed on hold.
— rsc for the proposal review group

@mvndaai
Copy link

mvndaai commented Apr 28, 2023

Any updates? I am still hoping for this any of these changes to make the return boilerplate inside an if err != nil cleaner. I have sometimes returned a *Struct{} instead of Struct{} so my returns can all be nil, err but that is a misuse.


To summarize the whole conversation:

The problem is with a return type that has multiple returns like (package.Struct{}, bool, error). Which is distracting to both type and read when a function has many error checks that end up looking like this.

if err != nil {
    return package.Struct{}, false, err
}

The original proposal was to allow ..., followed by any values.

return ..., err

Discussions on this on this included allowing ..., true, nil, the opposite package.Struct{}, ..., an empty .... The issue revealed is that it would make sense to allow ..., true, ... but that would be impossible for the compiler if there were multiple of the same return type in the middle like (any, any, any, error).

That led to the discussion of allowing zero value in the returns. Either as

return _, _, err

Or create a new Go builtin zero, like these other proposals #35966 and #52307. The zero would work as any zero value. Separating it from _ avoids confusion for people and the compiler.

return zero, zero, err

The complaint with a zero is if your return signature changes all the error lines also need to change.


I would be happy with any of them! I personally like the zero builtin because I would also use it in table unit testing.

@earthboundkid
Copy link
Contributor

I have sometimes returned a *Struct{} instead of Struct{} so my returns can all be nil, err but that is a misuse.

I was guilty of that last week as well. 😅

@afocus
Copy link

afocus commented Jun 28, 2023

Considering only the beginning and end, not the middle part may be more appropriate

resut,...
resut1,resut2,...
...,err
...,msg,err

@janpfeifer
Copy link

Something I didn't see any mention for was the pain of refactoring all the return error statements when a complex function with multiple return values requires changing the list of its return types (add or remove return elements). This proposal significantly simplifies this process.

@chad-bekmezian-snap
Copy link

I am fan of the ..., err syntax. That being said, I believe @narqo's comment is true. Most cases we are dealing with two values being returned, and _, err would be sufficient.

Either way, I would love to see this issue move forward!

@mvndaai
Copy link

mvndaai commented Nov 15, 2023

@Nasfame I personally think named returns are great for documentation but the amount of shadow variables they cause with := inside of ifs and fors makes using naked returns dangerous and cause more bugs than are worth the trade-off.

@mvndaai
Copy link

mvndaai commented Nov 19, 2023

@Nasfame I agree that the tooling, especially linters, has gotten much better. My issue is like you said, you needed "extensive usage over time" to prevent the bugs. I have coworkers who don't install all the tools in vim or are just new to Go. I don't like "naked returns" because it means I have to be constantly vigilant in my code reviews for a bug that wouldn't exist if you just didn't use them.

@vispariana
Copy link

Bringing this up again.
I'm personally in favor of the _ overload for several reasons.

  • Ellipsis is more associated with the spread operator in programming languages (such as our dear Go) while on the other hand, the underscore is usually used as ignore/skip/don't care. it feels more natural to me for this usecase.
  • Ellipsis is 3 characters, underscore is one!
  • Less complex to implement (how many - and where - ellipsis are allowed in a return statements)

What ellipsis offers is being able to skip multiple return values together, but let's face it: if you have functions with +3 return values, you have much bigger problems than the extra typing needed for returns. %99 of legit Go code I've seen returned either a single value, a value and an error, or in rare cases, two values and an error. And I can say that the latter case has almost always been the result of a rushed change or a lazy programmer.

For the underscore proposal, I also suggest preventing return _, _, a return statement (with the exception of no-return functions obviosly) must have at least one non-blank value. Returning "nothing" from a function that returns values is always a mistake because the caller is expecting "something" from you. Caller should have means to differentiate between intended zero values and skipped zero values. That's what the error does: if err is nil, then the value is intended even if it's zero. if err is non-nil, then the value should be discarded even if it's non-zero. _ should only be used as don't care, not zero. Otherwise it's a misuse.

Having said that, what about defining the behavior of _ as returning "some" value with the corresponding type? The initial implementation may be in fact zero, but by not guaranteeing a zero value we can prevent the misuse. And this may even enable some optimizations in the compiler. e.i when an underscore is returned, compiler can save some CPU cycles from unnecessary zeroing out bytes in the return segment of the stackframe and just let the caller get whatever value's already in there?

@ianlancetaylor ianlancetaylor added the error-handling Language & library change proposals that are about error handling. label Jan 7, 2024
@lorena-mv
Copy link

I would like to suggest using any... for returning multiple, consecutive zero values.

As @vispariana mentioned, the ... is more related to the spread operator. We could take advantage of this fact to spread the any keyword into multiple any values.

This has the advantage of being familiar syntax, and more readable for newcomers.

Examples:

return any..., err
return true, any..., err
return x, any...

By allowing only 1 instance of any... in a return statement, we ensure that the replacement is unambiguous.

@gregwebs
Copy link

@lorena-mv any outside of type constraints is equivalent to interface{} which would imply a conversion to an interface. True consistency would probably be something like

return [T any]T[]{...}..., err

which could perhaps be shortened to something like

return [any]..., err

Where this can potentially be generalized to allow the writing of a type in a return statement to give the zero value of that type as the return value (and ellipsis will return multiple of that value).

I think though that this is all more confusing than other alternatives already given here.

@gregwebs
Copy link

We have seen in this thread _ and ... but I don' think actually both. Here is a variation to support both:

return _, err // 1 value in addition to `err`
return _, _, err // 2 values in addition to `err`
return _..., err // any number of values in addition to `err`

@vispariana
Copy link

Hey @rsc , do you know if there is a plan to proceed with this proposal?

@earthboundkid
Copy link
Contributor

The proposal is on hold. It would need to come off hold and be approved by the Go proposal review team.

@vispariana
Copy link

The proposal is on hold. It would need to come off hold and be approved by the Go proposal review team.

I would like to know if there are more details on why this was put on hold last time, and if there are any plans to take it off hold again?

@atdiar
Copy link

atdiar commented Apr 11, 2024

@vispariana it requires to insersect several language features so it went on hold for more thoughts.
For instance, #66651, if accepted (and possibly extended to accept multiple variadic type parameters), could be used as-is or to implement such feature.

@coxley
Copy link

coxley commented Jul 2, 2024

To me, this proposal could have a side effect of encouraging bad API design.

I haven't come across many use cases where the number of zero values I'm returning as anything but an indicator that there's a better abstraction/type to return instead. Often the pain of managing that many return values (or arguments) is a feedback loop for a refector.

Using spread syntax in a completely different context doesn't feel like a worthy trade-off for this.

@nkcmr
Copy link

nkcmr commented Jul 17, 2024

I think this improvement has the opportunity to address more than just "make my code look nicer." So the retorts of "maybe it's time to clean up your return values!" have a dissonant ring to them.

I am eager to see either the spread ... or the _ be used to not just address visually noisy code, but to offer an unambiguous way for novices to return the zero-est of values for their code. (Slight preference for return _, _, err).

In my early days of Go, I remember briefly being confused by what to return when I had an error. Before learning about pointer's nil-ness I was trying to return nil for a non-pointer struct and was frustrated by compile errors.

Since then, I've seen countless times when folks in the early part of their learning of Go return non-zero values in error cases, like:

func f() ([]Data, error)
    if err != nil {
        return []Data{}, err // wrong! `return nil, err` is the zero-est possible return.
    }
    // ...
}

This is unlikely to cause a major bug, but there is also a non-zero chance it could. However, I do find this to be convincing evidence of the fact that learning what the "zero-est" possible value of some type in Go is non-trivial, and can slow down new learners.


On the preference of _ vs ...: Reserving the ... syntax for situations where data is actually being handled seems prudent. The _ token already has meaning and the connotation of "discard" or "skip," making it (in my opinion) more aligned with the connotation of this proposal.

@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed Proposal-Hold labels Aug 6, 2024
@vispariana
Copy link

Another argument I'd like to add in favor of this proposal is that I've seen far too many Go codes preferring to return pointers to structs instead of structs themselves, only because it was easier to return errors with return nil, err compared to return mypackage.SomeStruct{}, nil.

@vispariana
Copy link

Just came upon another usecase for this proposal.

consider generic functions or methods that return a type parameter and an error:

func Foo[T any]() (T, error) {
    if err := bar(); err != nil {
        // return the error, I don't care about the rest
    }
    return NewT(), nil
}

currently the available options are either

return *new(T), err

or

var empty T
return empty, err

or using naked returns which is the same as the latter option.

With this proposal, it can be done like any other function return:

return _, err

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dotdotdot ... error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
Status: Hold
Development

No branches or pull requests