I'd like to draft the following proposal for an expression used for error handling. I believe it achieved the following goals :
For disclosure, while I do think the try proposal needs improvement, it is on the right track as it covers the first two of the aforementioned points. I also don't think this will get any traction, though I do hope for some discussion, to at least know it was considered.
The draft introduces a new syntactic form: an optional or <identifier>: <Statement> expression added to the Return, Expression, Assignment and ShortValDecl statements.
Edit: after the first feedback, perhaps the grammar should be changed to or <identifier> <block> to better align with current syntax constructs.
This expression works similarly to try or check. If the preceding expression list produces one or more values, the last value is checked whether it is different than its zero value. If it is not, any previous values except it will be returned or assigned. If the last value is nonzero, it will be assigned to the identifier to be used in the scope of the following statement.
The now well known function can be rewritten as:
func CopyFile(src, dst string) error {
r := os.Open(src) or err: return err
defer r.Close()
w := os.Create(dst) or err: return err
defer w.Close()
io.Copy(w, r) or err: return err
w.Close() or err: return err
}
Edit:
func CopyFile(src, dst string) error {
r := os.Open(src) or err {
return err
}
defer r.Close()
...
}
Since the trailing part is a statement in itself, it can be used to decorate errors, handle them (in a code block if needed), breaking and continuing from loops, etc. It also doesn't allow nesting of invocations, which would otherwise reduce readability IMHO.
One obvious drawback is that it will be more difficult to implement and handle by various tools that a function.
It also may or may not be backwards compatible. Current keywords cannot be used as identifiers,
however this implementation could avoid defining or as another keyword. I do not have enough knowledge to know this if feasible.
While everything is up for debate, I haven't so far restricted the last value to only be of the error type. This construct seems handy when obtaining a value from a map as well - either obtaining it or dealing with its absence.
Edit:
Example:
func A(m map[string]interface{}) {
val := m["key"] or !ok {
log.Println("Key not found")
return
}
data := val.(MyType) or !ok {
log.Println("Key value is not of MyType")
return
}
...
}
I don't think the syntax fits well with the rest of the language. Currently an identifier followed by a colon is always a label or a case statement; here it is a variable declaration. Currently any conditionally executed statement is always in a block delimited by curly braces; here it is standalone.
I put : since to me, it visually seems to fit well. One solution would be to do what the handle part of the previous draft did:
... or err {
}
I would have preferred if the braces were not mandatory, but I understand the sentiment.
Has some clear similarities to #32848, although it's not identical.
Indeed. While very similar, I'd prefer to keep the try/catch behavior of shifting the last value. And I really do think this construct has potential usability for not just error types.
While this idea is interesting, it doesn't reduce boilerplate that much, compared to:
func CopyFile(src, dst string) error {
if r, err := os.Open(src); err ! = nil {
return err
}
defer r.Close()
}
True, but if you look closely, your code will not compile. It is also a bit harder to read, due to the initial if which will always force you to read the whole line to see if there is an initial statement hidden in there. And it's also why I prefer it a block is not mandated on the right side
You're right, I made a mistake, it needs to be like this:
func CopyFile(src, dst string) error {
if r, err := os.Open(src); err != nil {
return err
} else {
defer r.Close()
}
return nil
}
Admitted, an if with an initializer is a bit harder to read, and harder to write, but I don't think this proposal is such a significant improvement to warrant new syntax.
I have a very similar proposal https://github.com/golang/go/issues/33161 so I probably should have just collaborated with you in it.
My main concern with this proposal is that there isn't a visual cue at the beginning of the line that the function usually returns an error as its final parameter.
Questions:
Would you allow this in defer and go functions?
defer w.Close() or err {
// handle
}
go foo() or err {
// handle
}
md5-5d0de54a1030633946a78022a31f7d0e
a := foo.(assertionType) or !ok {
md5-a9a3440a3793a2a3d4adbb3066e07d5c
foo{
Val: (strconv.Atoi(s) or err {
return err
}),
}
Altering the defer and go statements could be useful, though it might make the implementation more difficult.
I do however prefer if this suffix works on any type, as stated in the addendum.
On the other hand, it's probably better if inlining isn't allowed. Your examples seems to reduce readability.
The || already means or in convention. It's not appropriate to use or to represent other things. I think I would consider another keyword.
This proposal introduces a new keyword or. I don't think it would be possible to make it not be a keyword.
I think the (modified) syntax is something like
Expression 'or' Identifier '{' Block '}'
The Expression must have at least one value. The Identifier is declared as the type of the last value of Expression. Then the Expression is changed to remove the last value. If the last value is not nil, then it is assigned to the newly declared variable, the Block is executed. The scope of Identifier is only Block.
Can this construct be used outside of an assignment statement? It would be awkward to have a block embedded in a larger expression. But only permitting this in an assignment statement does not seem very orthogonal.
This is a little bit shorter than writing the standard if statement, because you only have to write the identifier once, and you don't have to write != nil. But it's not all that much shorter.
This idea mixes together an assignment statement, an if statement, an implicit conditional test, and a variable declaration. In general, in Go, individual concepts are isolated and can be used independently. This is an odd hybrid of several different ideas.
Can this construct be used outside of an assignment statement? It would be awkward to have a block embedded in a larger expression. But only permitting this in an assignment statement does not seem very orthogonal.
I think that, in order to for this to be useful, it has to cover Return, Expression, Assignment and ShortValDecl statements. Which would, unfortunately, make the spec quite a bit more complex. That might be aleviated if the Expression 'or' Identifier '{' Block '}' was itself an expression, though I don't think that an expression can contain a statement.
In terms of length, I would still prefer that besides a Block, a simple statement should also be allowed for simple handlers.
As noted above, this is an hybrid of different ideas. And it requires a new keyword, which while not impossible, is a higher bar for a language change. It also introduces yet another assignment form. Therefore, this is a likely decline. Leaving open for four weeks for final comments.
-- for @golang/proposal-review
There were no further comments.
Most helpful comment
I don't think the syntax fits well with the rest of the language. Currently an identifier followed by a colon is always a label or a case statement; here it is a variable declaration. Currently any conditionally executed statement is always in a block delimited by curly braces; here it is standalone.