Description
Author background
- 1 year experience writing production Go code.
- Other language experience: Python, Typescritpt, Rust, C#.
Related proposals
This proposal is about else
statements in assign statements. It is exclusively borrowed from the let-else
statement introduced in Rust RFC 3137, which is also based on Swift's guard
statement.
I haven't been able to find a similar existing proposals. Please close if duplicated.
Proposal
The goal of this proposal is to introduce a guard to simplify some very common error-handling patterns we frequently encounter when writing Go code. A short-hand way to return errors has been proposed a few times already (See #56628, #32437, #56165). I believe the existing proposals don't match 100% Go's mission for simplicity, correctness and readability.
Although introducing this feature could go against R. Pike's comment Go does not try to be like the other languages
(See https://go.dev/talks/2015/simplicity-is-complicated.slide#9) I think the benefit is hard to ignore.
Code examples:
// before
result, err := getUser()
if err != nil {
return err
}
exp, ok := number.(uint64)
if !ok {
return fmt.Errorf("not uint64")
}
// after
result, err := getUser() else {
return err
}
exp, _ := number.(uint64) else {
return fmt.Errorf("not uint64")
}
The new else
statement in a variable statement is easy to read and understand. It allows the developer to write code and think of its immediate error handling by moving the failure case into the body of the assign expression itself.
Spec
Only the last value is evaluated as the else
condition. The last value must implement the error
interface or be type bool
ret1, ret2, err := test() else {
return err
}
ret1, err1, err2 := test() else {
// err2 is not nil
return err2
}
func testA() (error, string)
err, s := testA() else { // Compilation error. `s` does not implement `error` interface
return err
}
Variable scope works as expected. Variables are not scoped to the else
block
ret, err := testA() else {
return err
}
// reuse err
ret2, err := testB() else {
return err
}
err = testC() else {
return err
}
Only available in assign
statements
// Invalid: no assign statement
testC() else {
return err
}
Only valid for a single expression on the right hand of the assignment
a, b := testA(), testB() else { // invalid
...
}
It is possible to omit the last returned value in type assertions and map access (as it is already possible)
exp := number.(uint64) else {
return fmt.Errorf("not uint64")
}
m := map[string]string{}
value := m["value"] else {
return fmt.Errorf("value not found")
}
Motivation
Go's simplicity is great but that simplicity usually come with a cost of repetition, especially when talking about error handling.
Go does not support pointer to boolean conversion hence developers have to write the following block constantly
ret, err := some()
if err != nil {
return err
}
Although, this can be easily solved with a code snippet, codebases tend to be saturated by
if err != nil {
return err
}
statements.
The proposal avoids repetition of the if err != nil
code. There is no nil to bool conversion as the else
statement would internally evaluate err != nil
as its condition.
Readability is maintained or improved if considered that error handling is now part of the assignment statement.
Please refer to Rust RFC 3137 for more details on its motivation as I believe Go developers would agree with them.
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 automated tooling will desugar it.
Costs
What is the cost of this proposal?
An additional rule to understand when assigning variables. Specifically the rule of evaluating only the last result returned by the right hand of the assign statement.
Would this change make Go easier or harder to learn, and why?
Go is already a very simple language to learn. The intention of this new assign expression could seem natural to understand.
The new expression is also intended to be optional and more of a syntax sugar.
How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
Linters and formatters would require to be extended to support the new assign expression.
What is the compile time cost?
Probably none. Some compilation step could be affected but it depends on the implementation. A naive approach could imply a desugar
step of the source code. Otherwise the cost would reside during parsing step, modification to the AssignStmt in Go's AST or a new type of expression would be required.
What is the run time cost?
None
Can you describe a possible implementation?
I imagine extending the AssingStmt
with an Else
statement field (similar to Else in IfStmt).
The evaluation of this Else
statement should work as expected when finding a return
statement.