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: handle error in minimal way #57552

Closed
wzzhu opened this issue Jan 3, 2023 · 20 comments
Closed

proposal: handle error in minimal way #57552

wzzhu opened this issue Jan 3, 2023 · 20 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@wzzhu
Copy link

wzzhu commented Jan 3, 2023

It just cuts the lengthy if err !=nil {return ... nil} away without introducing any new syntax/keyword/symbol/func, based on go compiler support that inserts the implicit return/log statements for unhandled error.

if err := f(); err !=nil {
    return err
}

to

err := f()

Would you consider yourself a novice, intermediate, or experienced Go programmer?

12 years of Golang experience (use before it was officially released)

What other languages do you have experience with?

C/C++ 20+ years, Objective-C 12+ years, shell script 20+ years
C#, python, Java, Javascript: familiar for 10-20 years but not frequently used.
Assembly/Fortran/Basic/Pascal/Cobol: known for many years, but rarely used

Would this change make Go easier or harder to learn, and why?

It is easier to learn and makes go programs more simplified in its verbose error handling.
It adds no keywords, just "hide" the boilerplate error handling code with compiler support. It is like ARC in Objective-C without explicit release statement.

Has this idea, or one like it, been proposed before?

To the best of my knowledge, it has not been proposed.

If so, how does this proposal differ?

This proposal introduces no new keywords/symbols/syntax/functions (in contrast to check/handle, try, ?, etc. solutions proposed).

Who does this proposal help, and why?

Any golang user. It provides a nonintrusive error handling method.

What is the proposed change?

Propose to use compiler to auto generate return or log statement for any unhandled error value. "unhandled" means that its value after last write is not used.

If a func has an error return value, compiler will auto generate the return <empty value list of the other parameters + unhandled error value (optionally adding the func name with wrapped error to help debugging). Otherwise it generates log.Print(err) and return.

Please describe as precisely as possible the change to the language.

Compilers need to be changed to handle the error, it needs to know what error interface is and treats it specially.
Also language extension/plugins & editor can be enhanced to toggle the display(or show in grayed) the implicitly generated code

What would change in the language spec?

Describe the behavior of unhandled error value.

Please also describe the change informally, as in a class teaching Go.

Go compiler will auto generate return code for unhandled error.

Is this change backward compatible?

Yes.

Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit.

Show example code before and after the change.

Examples taken from standard golang lib encoding/json

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        if err != nil {
                return nil, err
        }
        var buf bytes.Buffer
        err = Indent(&buf, b, prefix, indent)
        if err != nil {
                return nil, err
        }
        return buf.Bytes(), nil
}

can be changed(simplified) to

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        var buf bytes.Buffer
        err = Indent(&buf, b, prefix, indent)
        return buf.Bytes(), nil
}

What is the cost of this proposal? (Every language change has a cost).

Compiler may need to analyze the unhandled error and it needs to generate the return statements for unhandled error.

How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?

vet (for warning of unused error value). Debugger needs to know the implicit control flow. Language extension for editor, or IDE need to change to know the hidden return statements as well.

What is the compile time cost?

The analyzer's time will be increased but it is inside functions only. Minimal additional imports (log in standard lib) won't increase much time.

What is the run time cost?

Zero additional cost

Can you describe a possible implementation?

Add a pass for compiler to scan each func's syntax tree to check unhandled error values, and add the return statements, treating the func return as a write to error values.

Do you have a prototype? (This is not required.)

No

How would the language spec change?

Add statement to explain the implicit error handling.

Orthogonality: how does this change interact or overlap with existing features?

Totally orthogonal.

Is the goal of this change a performance improvement?

No

If so, what quantifiable improvement should we expect?
How would we measure it?
Does this affect error handling?

Yes, it automates and simplifies error handling

If so, how does this differ from previous error handling proposals?

No new keyword introduced to avoid the cognitive burden.

Is this about generics?

No

If so, how does this relate to the accepted design and other generics proposals?
What to avoid

@wzzhu wzzhu added the Proposal label Jan 3, 2023
@gopherbot gopherbot added this to the Proposal milestone Jan 3, 2023
@seankhliao seankhliao added LanguageChange v2 A language change or incompatible library change error-handling Language & library change proposals that are about error handling. labels Jan 3, 2023
@seankhliao
Copy link
Member

Please fill out https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing language changes

@seankhliao seankhliao added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 3, 2023
@fsouza
Copy link
Contributor

fsouza commented Jan 3, 2023

What's the expected behavior of the snippet below? (adapted from your proposal)

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        var buf bytes.Buffer
        err = Indent(&buf, b, prefix, indent)
        return buf.Bytes(), err
}

@wzzhu
Copy link
Author

wzzhu commented Jan 3, 2023

What's the expected behavior of the snippet below? (adapted from your proposal)

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        var buf bytes.Buffer
        err = Indent(&buf, b, prefix, indent)
        return buf.Bytes(), err
}

The compiler will not complain the above code as an error due to unused error variables and will generate code with same semantics as the original code.

@seankhliao seankhliao added WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. and removed WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. labels Jan 3, 2023
@seankhliao
Copy link
Member

What does this generate?

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        foo(err)
        return b, nil
}

@ianlancetaylor
Copy link
Contributor

This change is not backward compatible.

Code like this is valid today, but would change behavior under this proposal.

        b, err := Marshal(v)
        err = F()
        return err

@redsuperbat
Copy link

redsuperbat commented Jan 3, 2023

A possible solution to this would be to tell the compiler where it should generate the returns.
Taking inspiration from rust the code would look something like this instead:
(the ? question mark would indicate an early return)

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, _ := Marshal(v)?
        var buf bytes.Buffer
        Indent(&buf, b, prefix, indent)?
        return buf.Bytes(), nil
}

Correct me if I'm wrong but this would not break existing code I don't think?

@seankhliao
Copy link
Member

see #51146

@redsuperbat
Copy link

Thanks for pointing me in the right direction @seankhliao.

Nothing new under the sun ☀️ 😹

@wzzhu
Copy link
Author

wzzhu commented Jan 4, 2023

What does this generate?

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        foo(err)
        return b, nil
}

This won't change anything as err value is used by foo.

@wzzhu
Copy link
Author

wzzhu commented Jan 4, 2023

This change is not backward compatible.

Code like this is valid today, but would change behavior under this proposal.

        b, err := Marshal(v)
        err = F()
        return err

Yes, thanks, the behavior is changed for this code. It was overlooked.
Some workarounds possible to add hints to compiler? Think of some but not very ideal. Throw a sprat below.

  1. Add go directive like //go:autoerrorhandling at file level. Seems lightweight, but still not natural enough not to set by default.
  2. Use go.mod directives. Shortage: setup burden.
  3. Rename new files with extension to .go2. Shortage: affect too many tools

@ianlancetaylor
Copy link
Contributor

I want to be clear that we aren't going to do any of those things. Breaking compatibility with existing code is a huge cost. It would require a huge benefit. This suggestion is at best a small benefit, not nearly sufficient to break compatibility.

@ydstoi
Copy link

ydstoi commented Jan 4, 2023

A possible solution to this would be to tell the compiler where it should generate the returns. Taking inspiration from rust the code would look something like this instead: (the ? question mark would indicate an early return)

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, _ := Marshal(v)?
        var buf bytes.Buffer
        Indent(&buf, b, prefix, indent)?
        return buf.Bytes(), nil
}

Correct me if I'm wrong but this would not break existing code I don't think?

I have a similar idea.

func foo()error{
         res, err? := bar() defer handleMore(err)
}

It is equivalent to:

func foo()error{
         res, err := bar()
         if err!=nil{
                  defer handleMore(err)
                  return err
         }
}

It is simple, explicit, and extends defer to handle additional tasks before returning.

@seankhliao
Copy link
Member

see #36390

@ianlancetaylor
Copy link
Contributor

A significant change in semantics based on whether or not a variable is used is too confusing for a language like Go.

The later suggestion of a ? has been made before as @seankhliao points out.

Based on this, and the discussion above, and the emoji voting, this is a likely decline. Leaving open for four weeks for final comments.

@senluowx
Copy link

senluowx commented Jan 7, 2023

A possible solution to this would be to tell the compiler where it should generate the returns. Taking inspiration from rust the code would look something like this instead: (the ? question mark would indicate an early return)

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, _ := Marshal(v)?
        var buf bytes.Buffer
        Indent(&buf, b, prefix, indent)?
        return buf.Bytes(), nil
}

Correct me if I'm wrong but this would not break existing code I don't think?

I have a similar idea.

func foo()error{
         res, err? := bar() defer handleMore(err)
}

It is equivalent to:

func foo()error{
         res, err := bar()
         if err!=nil{
                  defer handleMore(err)
                  return err
         }
}

It is simple, explicit, and extends defer to handle additional tasks before returning.

multiple returns

func foo() (any, error){
    res, (nil, err)? := bar()
}

or

func foo() (any, error){
    res, carry (nil, err) := bar()
}

@ydstoi
Copy link

ydstoi commented Jan 7, 2023

@senluowx It seems unnecessary to complicate things further. I know that my plan is not accepted and the reason, but I still want to explain. Some previous issues (such as #37174 ) mentioned only returning an error, and others using the default zero value in go, such as the case you mentioned. So my idea mentions extending defer, because if you just simplify return, you can't handle some extra operations before return only when errors occur, and if there is a non-zero value that needs to be returned, you can assign a value after defer in the way of a named return value.

@wzzhu
Copy link
Author

wzzhu commented Jan 17, 2023

I had an idea to extend original suggestion by re-using the existing keyword "return" as modifier to the error var to be managed. i.e.,

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, err := Marshal(v)
        if err != nil {
                return nil, err
        }
        var buf bytes.Buffer
        err = Indent(&buf, b, prefix, indent)
        if err != nil {
                return nil, err
        }
        return buf.Bytes(), nil
}

becomes

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
        b, return err := Marshal(v)
        var buf bytes.Buffer
        return err = Indent(&buf, b, prefix, indent)
        return buf.Bytes(), nil
}

It keeps the backward compatibility as "return var" as a left-value won't be conflict with existing code, and the original keyword return exists in different context from current one which is in a left-value.

Without "return" modifier, the err value would be handled in old familiar way.

Also "return" can be used for any number of error variables. So that compiler will handle it one by one according to text order. e.g.,

return err1, return err2, a, b, err := f(c, d)
will be corresponding to

err1, err2, a, b, err := f(c, d)
if err1 != nil {
   return _, err1
}
if err2 != nil {
   return _, err2
}

For me it has less mental burden to use "return" modifier than any other cryptic symbols (like ?, or !) or new keywords/func(check, try) and it still looks like normal go code. The "return" keyword looks like compressing if err statement{return _, err} into a single keyword "return", which is a natural reminder of a control flow exists here: if the error value is not nil, it will be handled automatically by compiler.

But I am not sure if the idea falls into existing bad cases or others may feel strange. Welcome comments. Thanks.

PS:
Found that it was similar to this declined issue, which uses a new keyword "check" in the place of "return" here #46655

Another issue suggesting "pass err", and even "return? err" or "return if err" #37141

@xiaokentrl

This comment was marked as off-topic.

@ianlancetaylor

This comment was marked as resolved.

@ianlancetaylor
Copy link
Contributor

No change in consensus.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Feb 2, 2023
@golang golang locked and limited conversation to collaborators Feb 2, 2024
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 Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

9 participants