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: add "or err: statement" after function calls for error handling #33029

Open
urandom opened this issue Jul 10, 2019 · 12 comments

Comments

@urandom
Copy link

commented Jul 10, 2019

I'd like to draft the following proposal for an expression used for error handling. I believe it achieved the following goals :

  1. It reduces verbosity associated with error handling
  2. It provides separation between business and error logic without hiding any
  3. It is intuitive and should be easy to understand even without prior knowledge.
  4. it clearly shows what is about to happen in any branch.
  5. Useful in functions that do not return errors - such as http handlers.

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.

Design

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.

Drawbacks

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.

Addendum

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  
   }

  ...
}

@gopherbot gopherbot added this to the Proposal milestone Jul 10, 2019

@gopherbot gopherbot added the Proposal label Jul 10, 2019

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Jul 10, 2019

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.

@urandom

This comment has been minimized.

Copy link
Author

commented Jul 10, 2019

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.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Jul 10, 2019

Has some clear similarities to #32848, although it's not identical.

@urandom

This comment has been minimized.

Copy link
Author

commented Jul 10, 2019

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.

@beoran

This comment has been minimized.

Copy link

commented Jul 11, 2019

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()
}
@urandom

This comment has been minimized.

Copy link
Author

commented Jul 11, 2019

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

@beoran

This comment has been minimized.

Copy link

commented Jul 11, 2019

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.

@ianlancetaylor ianlancetaylor changed the title Proposal: an expression useful for error handling. proposal: Go 2: add "or err: statement" after function calls for error handling Jul 17, 2019

@mvndaai

This comment has been minimized.

Copy link

commented Jul 18, 2019

I have a very similar proposal #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
} 

Could I use this with an ok?

a := foo.(assertionType) or !ok {

Could it be used inline?

foo{
    Val: (strconv.Atoi(s) or err {
        return err
    }),
}
@urandom

This comment has been minimized.

Copy link
Author

commented Jul 19, 2019

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.

@DongchengWang

This comment has been minimized.

Copy link

commented Jul 20, 2019

The || already means or in convention. It's not appropriate to use or to represent other things. I think I would consider another keyword.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Sep 17, 2019

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.

@urandom

This comment has been minimized.

Copy link
Author

commented Sep 18, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.