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: Go 2: just return the error #56628

Closed
gregwebs opened this issue Nov 7, 2022 · 6 comments
Closed

proposal: Go 2: just return the error #56628

gregwebs opened this issue Nov 7, 2022 · 6 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@gregwebs
Copy link

gregwebs commented Nov 7, 2022

Author background

  • 5 years experience writing production Go code. I have forked and created Go error handling libraries.
  • Other language experience: Many of the common languages that are not JVM/CLR (Rust, Haskell, Python, Ruby, TypeScript, SQL, bash, etc)
  • I have opened other error handling proposals and reviewed many of the existing proposals- this experience has led me to realize that we really just need to fix how returning errors also requires returning zero values.

Related proposals

  • This proposal is about error handling
  • There is a very similar proposal to this that is still open. This proposal does not introduce a new syntax but instead relaxes the requirements for multiple return parameters when returning an error.

Proposal

When returning an error, one may omit the rest of the return values.

// before
return mystruct{}, package.PackageStruct{}, err

// after
return err

This new form matches the intent that "I just want to return an error".
The new form is optimal to read, write, and diff for the purpose of returning an error.

This form is allowed if and only if the last return value satisfies the interface error.
The value of the omitted return values will be the zero value of their type.

Motivation

The benefits to readability will be seen in every multi-valued error return statement, which is a significant amount of existing Go code.
I see Go programmers returning structs as pointers because nil is easier than hassling with putting empty structs at every return, so unfortunately the existing situation contributes to making the code messier outside just the return statement.

Reading, writing, diffing

  • Reading: Existing code obscures the intent when reading- the reader needs to check that all the non-error values are in fact zeros and then ignore them.
  • Writing: Writing out zero values is tedious but can be automated or atleast auto-completed by IDEs.
  • Diffing: Diffs can be unnecessarily large- if I make a one line change at the bottom of my function to return one additional result, the only additional change the reviewer needs to see is the diff of the type signature. But the diff will also show adding zero values on all the rest of the return lines.

Orthogonality

This proposal could be viewed as an extension/specialization to the existing concept of a naked/bare error return.
Because of this, it may be best to use named return variables when they exist rather than only returning zero values.

This feature is specialized to errors. It is possible to attempt to generalize this feature to support using it to return any return result type that is uniquely typed. However, such a generalization is rarely useful outside of bare/naked returns and bare/naked returns are not used with high frequency in the Go community.

Compatibility

Old code will continue to compile on newer versions of Go.
New code will not compile on older versions of Go unless this feature is back-ported to older releases, or the new code is modified with automated tooling.

Costs

  • What is the cost of this proposal? (Every language change has a cost).
    An additional rule to understand. Outside of the interaction with named results, this rule should be intuitive.

  • Would this change make Go easier or harder to learn, and why?
    Some will argue this makes Go harder to learn because it is a new additional feature. I would argue it will make it much easier to learn Go because it will be significantly easier to read error return statements- that saved time will make it possible to spend more time learning Go.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
    Most linters that use the Go analysis library could continue to work without change for most of their lints.
    However, some lints focused on the specifics of returns would require changes. It will now be easier to perform autofixes or suggestions that add an error return- these no longer need to bother with zero values.

  • What is the compile time cost?
    Probably none? In theory an integrated compiler could compile this (probably not noticeably) faster since there is less to parse and returns must be type-checked already.
    However, the separation of compilation phases might mean the effect would be the opposite.

  • What is the run time cost?
    None, in theory this might make some optimizations easier.

  • Can you describe a possible implementation?
    No, but I imagine the machinery for this is probably somewhat in place already due to the existence of bare/naked returns.

  • Do you have a prototype? (This is not required.)
    I have a library that panics (and recovers) an error. This achieves the same result of omitting zero values.

@gopherbot gopherbot added this to the Proposal milestone Nov 7, 2022
@gregwebs gregwebs changed the title proposal: Go 2: proposal: Go 2: just return the error Nov 7, 2022
@seankhliao seankhliao added LanguageChange v2 A language change or incompatible library change error-handling Language & library change proposals that are about error handling. labels Nov 7, 2022
@andig
Copy link
Contributor

andig commented Nov 7, 2022

You can‘t beat that for simplicity. It does not feel right though to make err special here, given that it is just another return parameter with no formal significance.

@gregwebs
Copy link
Author

gregwebs commented Nov 7, 2022

@andig thanks for voicing that concern. I guess there is a point of view that Go is just returning multiple values, and there is no reason to formalize the return of errors in any way.

The point of view that I am representing is that the code is not actually trying to return multiple values. The code is trying to return a success or an error, exclusively. Some functions are exceptions to this general pattern, and those exceptions should not use this feature, but what is described here represents the majority of functions in some code bases.

AFAICT this can only really only be generalized to actually adding a Result types to Go (or more generally a sum type), which would be an enormous challenge. This proposal describes the one reliable convention for using sum types in Go. The benefits of sum types are clear to anyone that has experience using them, but it is not actually clear that a Result type would produce easier to read code for this common pattern than what Go would have after this proposal.

@gregwebs
Copy link
Author

In Zig a function can just return an error (or a success). The result type is formalized with ! as syntax in the type signature indicating that the function returns an error and there are also tools on the caller side for early return or pattern matching the error.

pub fn parseU64(buf: []const u8, radix: u8) !u64 {

The ! return result approach would work quite well in Go, but it's a more dramatic syntax addition. We could avoid adding a real result type for now and ! could be added such that it effectively compiles down to the current state of affairs where the right-most value is an error. But crucially this new ! syntax would allow for what is contained in this proposal. Additionally, when returning a success, one would omit the error rather than return nil.

The return type would ideally be a concrete error set as in Zig rather than erased into an error. It's currently very problematic in Go to return a concrete error due to the boxed dynamic type behavior: a nil concrete error type is not equal to a nil error (but these invalid comparisons will still compile). This behavior could be fixed for errors when using !.

@DeedleFake
Copy link

The point of view that I am representing is that the code is not actually trying to return multiple values. The code is trying to return a success or an error, exclusively. Some functions are exceptions to this general pattern, and those exceptions should not use this feature, but what is described here represents the majority of functions in some code bases.

An argument could even be made that standardizing the use of a result type for the usual case of value or error will signal to the client that a function that isn't using the result type might need some special handling. It may be too late for that benefit, though, since the standard library is filled with multi-return value-or-error functions already. Still, I'd personally really like to have functionality similar to the following available, though it would be dependent on #56462 or something similar:

type Result[T ...any] struct {
  v T
  err error
}

// Just doing some bikeshedding...
func R[T ...any](v T, err error) Result[T] { /* Converts from an existing multi-return function. */ }
func Success[T ...any](v T) Result[T] { /* Straightforward. */ }
func Err[T ...any](err string) Result[T] { /* Equivalent to errors.New(). */ }
func (r Result[T]) Must() T { /* Panic if there's an error. */ }
func (r Result[T]) IgnoreErr() T { /* Just return the value. */ }
func (r Result[T]) Or(v T) T { /* Return v if the error wasn't nil. */ }
func (r Result[T]) Err() error { /* Return the error, whether or not it's nil. */ }
func (r Result[T]) Unwrap() (T, error) { /* Return everything. */ }

// Elsewhere:
package fmt

func Resultf[T ...any](format string, args ...interface{}) { /* ... */ }

Unfortunately, the Err() and fmt.Resultf() constructors would be significantly less useful without some extensions to the generic type inference system. As it stands currently, you'd have to do something like return fmt.Resultf[time.Time]("parse time: %w", err), which would be annoying.

@gregwebs

That would require such a major overhaul of the type system that it seems extremely infeasible to me to implement for use with a single feature. I think an actual result type is far more likely to happen at some point.

@gregwebs
Copy link
Author

I like the idea of the Unwrap api that goes from a sum type to multi-value with zeroes- it's the way to preserve compatibility, although I would name it different given its existing usage for errors!

It's easy to come up with the return result and such an API but it is harder to explain how this will actually be better on the caller side than what exists today. Rust had to add ? for early returns to avoid lots of nesting from matching, etc.

@ianlancetaylor
Copy link
Contributor

It's good to make the eliding of values explicit, to avoid unexpected behavior if the function results change.

Based on the discussion, and the emoji voting, and the similarity to #21182, closing as a dup of #21182.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Dec 7, 2022
@golang golang locked and limited conversation to collaborators Dec 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

6 participants