- Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
Proposal Details
Throwing my hat into the already crowded ring for Go error handling with an extremely simple proposal: suffixing ? to a variable name in an assignment statement adds an implicit if (var != zeroval) { goto var-as-label; }.
Go proposal template attached at end.
Code Sample
@rsc's CopyFile example from https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md becomes:
func CopyFile(src, dst string) error { r, err? := os.Open(src) defer r.Close() w, err? := os.Create(dst) defer w.Close() _, dstErr? := io.Copy(w, r) dstErr? = w.Close() return nil dstErr: os.Remove(dst) err = dstErr err: return fmt.Errorf("copy %s %s: %v", src, dst, err) }Notes
-
Go has separate namespaces for labels and variables, so there's no conflict between the two already. Try it on the playground!
-
Allows arbitrary composition of error handling logic using normal golang control flow. Re-use error variable names to re-use error handling logic, use different variable names to support unique error handling logic, order them appropriately to chain handlers together, short circuit fallthrough with a return, use goto for more complex ordering.
-
No support for
if (var == zeroval) { goto var-as-label; }. Could potentially allow ! prefix in labels to support this form, for instance, to support the common golang ok idiom:value, ok? := <-ch return value, nil !ok: return value, errors.New("channel closed")
-
More than one ? test in a multiple assignment statement should be a compiler error. Use standard
if errtests when multiple tests are needed in one assignment so control flow is clear.err1?, err2? := io.ErrShortWrite, errors.New("some other error") // ... err1: // should control jump here? err2: // or here?
-
I considered a switch/case-like version:
func CopyFile(src, dst string) (int, error) { r, err? := os.Open(src) defer r.Close() w, err? := os.Create(dst) defer w.Close() _, dstErr? := io.Copy(w, r) dstErr? = w.Close() return bytes, nil when dstErr != nil: os.Remove(dst) err = dstErr fallthrough when err != nil return 0, fmt.Errorf("copy %s %s: %v", src, dst, err) }
But I don't see many advantages over the goto based version to justify the additional language spec/implementation complexity. (2024/01/08 -- broke this out into a separate proposal anyway: proposal: spec: support new form of switch statement during variable assignment which jumps to function-wide case blocks #65019)
Updates
-
2024/01/04: After I wrote this, it came to my attention that golang doesn't support goto jumping over variable declarations. I've used goto in several golang programs but somehow never stumbled across that issue, so I was unaware. However, commenters pointed out that there is an open proposal to address that issue: proposal: spec: permit goto over declaration if variable is not used after goto label #26058. This would need implemented in order to properly support this feature.
-
2024/01/04: @seankhliao pointed out there is a gist with an essentially identical proposal ( https://gist.github.com/dpremus/3b141157e7e47418ca6ccb1fc0210fc7) that was never submitted.
-
2024/01/04: @seankhliao also pointed out there was a proposal (proposal: Go 2: extend "goto" - statement #53074) that also re-used variable names as labels. @ianlancetaylor didn't like overloading variable names with labels, though golang currently permits it.
I do like the brevity of re-using the error variable as the label, less typing and cleaner looking code. Using typical go conventions, this would always be 'err' or '*Err' so it would be clear that this was an 'error goto' destination without having to use any special keywords. Could even enforce that with a 'go vet' check. It also means the error labels can be used for regular goto statements, which is nice for complex error handling chaining. If we need to distinguish the implicit error jump labels from standard goto labels, though, I propose this version for symmetry:
func CopyFile(src, dst string) error { r, err? := os.Open(src) defer r.Close() w, err? := os.Create(dst) defer w.Close() _, dstErr? := io.Copy(w, r) dstErr? = w.Close() return nil ?dstErr: os.Remove(dst) err = dstErr ?err: return fmt.Errorf("copy %s %s: %v", src, dst, err) }
If this version is preferred, then I would also propose that we add the ability for the regular goto statement to jump to ? labels, as in 'goto ?err', to support complex error chaining.
-
2024/01/04: @carlmjohnson asked if the label can appear before the assignment. I don't see any reason to use different rules for the implicit ? goto labels than standard goto and labels. Consider using the feature to implement error retry:
attempts := 0 var err error err: attempts++ if attempts > 5 { return nil, errors.New("failed after 5 attempts with: %w", err) } if err == context.Canceled { return nil, err } resource, err? := getResource() result, err? := getData(resource) return result, nil
If you tried something like this with a for loop, you'd need checks after each function to either break or continue. The more functions in the retry chain the more boilerplate you'd save using the ? feature.
-
2024/01/12 -- @apparentlymart and @findleyr brought up cases like this:
func _() { if cond { err? := "hi" } err := 1 ?err: // What is the type of err here? }
but if we apply the 'it's just a normal goto` test, you can see that even today, the compiler already gives you a good error, as tested on the playground. Couldn't put a link for some reason, getting a server error when I click the share button.
func main() { if true { // err? := "hi", translated to: err := "hi" goto err } err := 1 err: fmt.Println("Hello,", err) } // compilation error: ./prog.go:10:8: goto err jumps over declaration of err at ./prog.go:13:6
Which is great, because you're writing confusing code. But it's a simple fix. a) don't do that because you wouldn't be able to with a normal goto, and b) just declare err as a variable in the outer scope.
func main() { var err string if true { // err? = "hi", translated to: err = "hi" goto err } // can't do this anymore, but that's probably a good thing for code clarity // err = 1 err = "everything's fine now" err: fmt.Println("Hello,", err) } // Prints 'Hello, hi'.
What I like so much about this proposal is the simplicity of it, which is very appealing to me. It's not inventing any new forms of flow control a new developer has to learn. After thinking through all these different cases, I keep coming back to 'it's just a normal goto'.
Go Proposal Template
-
Would you consider yourself a novice, intermediate, or experienced Go programmer?
Experienced. -
What other languages do you have experience with?
Assembly, C, C++, Python, Erlang, several others -
Would this change make Go easier or harder to learn, and why?
A bit more control flow to learn with new ? suffix, but by moving all the error handling boilerplate to separate blocks, the main logic would be easier to follow, so could argue either way. -
Has this idea, or one like it, been proposed before?
I read through as many error handling proposals as I could and did my best with the github search feature but couldn't find anything as simple and straightforward. -
Who does this proposal help, and why?
All gophers that would like more concise error handling. -
Is this change backward compatible?
Yes, existing code does not need to be changed. -
What is the cost of this proposal? (Every language change has a cost).
More complex grammar, -
How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
Probably many, since this would be a change in language syntax. -
What is the compile time cost?
Not familiar enough with the compiler internals to answer this. -
What is the run time cost?
Probably negligible if the compiler can optimize out unnecessary case tests. -
Do you have a prototype? (This is not required.)
No, but would be happy to put one together with a little guidance. -
How would the language spec change?
Only adding support for a ? variable suffix. -
Orthogonality: how does this change interact or overlap with existing features?
Re-uses existing golang control flow so minimal interactions. -
Does this affect error handling?
This change is intended to provide a new control flow mechanism during variable assignment that can be used to make error handling less verbose, but is not exclusive to error handling. -
If so, how does this differ from previous error handling proposals?
From spec: error handling meta issue #40432, seems similar to those with this classification: 'Special characters, often ! or ?, that insert an error check in a function call or assignment.', but none of the ones I could find are as dead simple.