go: proposal: Go 2: `on err` or `catch` statement for error handling
The try() proposal #32437 has met with significant complaint, especially about
a) lack of support for decorating/wrapping returned errors
b) lack of support for widespread alternatives to returning error, e.g. log.Fatal(), retry, etc.
c) obscured function exit
d) difficulty of inserting debugging logic
If a separate line is usually required for decoration/wrap, let’s just “allocate” that line.
f, err := os.Open(path) // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)
(This may overlap too much with if ... { <stmt> }. An error-specific variant is given in Critiques no. 1.)
This plugs easily into existing code, as it leaves the err variable intact for any subsequent use.
This supports any single-statement action, and can be extended with named handlers:
err := f() // followed by one of
on err, return err // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err) // doesn't stop the function
on err, continue // retry in a loop
on err, goto label // labeled handler invocation
on err, hname // named handler invocation
on err, ignore // logs error if handle ignore() defined
handle hname(clr caller) { // type caller has results of runtime.Caller()
if err == io.Bad { return err } // non-local return
fmt.Println(clr, err)
}
Keyword on borrows from Javascript. It’s preferrable not to overload if.
A comma isn’t essential, but semicolon doesn’t seem right. Maybe colon?
Go 1 boilerplate is 20 characters on 3 lines: if err != nil {\n\t\n}\n
This boilerplate is 9 characters on 1 line: on err, \n
Specifics
// Proposed
on expression, statement
// Equivalent Go
var zero_value T // global, not on stack
if expression != zero_value {
statement
}
// Disallowed; these require a named handler or function call
on expression, if/for/switch/func { ... }
on expression, { statement; statement }
The expression is constrained to ensure that its use (if any) within the statement is correct. For example, expression cannot be a function call or channel receive. It could be limited to an lvalue, literal, or constant,
Possible Extensions
For an assignment or short declaration with a single lvalue, on could test that value. (Note, I don’t recommend this; better to have on err consistently on its own line.)
on err := f(), <stmt>
An error inspection feature would be helpful…
on err, os.IsNotExist(err): <stmt>
on err, err == io.EOF: <stmt>
on err, err.(*os.PathError): <stmt> // doesn't panic if not a match
on err, <condition>: <stmt>
on err: <stmt> // this pair provides if/else in 2 lines
A condition list suggests parentheses:
on (err) <stmt>
on (err, <condtion>) <stmt>
Critiques
-
The above may be too general-purpose. A post-assignment alternative is:
err := f() catch <stmt> // test last var in preceding assignment for non-zero catch (err == io.EOF) <stmt> // test last var and boolean condition catch (ep, ok := err.(*os.PathError); ok) <stmt> // assignment preceding condition -
This doesn’t accommodate narrowly scoped error variables
if err := f(); err != nil { ... }. A way to scope the variable to the stmt is:_ = f() catch err, <stmt> catch err, (err != io.EOF) <stmt> -
go fmt could just allow single-line if statements: Would it also allow single-line
case, for, else, var ()? I’d like them all, please 😉 The Go team has turned aside many requests for single-line error checks. -
It’s as repetitive as the current idiom:
on err, return errstatements may be repetitive, but they’re explicit, terse, and clear.
@gopherbot add Go2, LanguageChange
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 59
- Comments: 94 (38 by maintainers)
I would opt for a
,=reverse nil operator that does the same but keeps the clean line of side@griesemer
I’m sorry to have overemphasized
checkErr, I didn’t mean to suggest it was the default case in code that I review, I only wanted to use it as an example of how error handing in “real world” Go is probably a lot less sophisticated than one might think. I agree thattryis a step up fromcheckErr, but then again, any use of returned errors instead of blanket panics would be an improvement.I’m pretty sure more explanation from me won’t help to sell my case any more at this point, but I do want to respond to this:
Prior to the check/handle proposal, I had never once considered, or seen in the wild, the general pattern of decorating errors with a deferred function. (The only times I’d ever used a deferred function to interact with errors were in the exceptional cases where that pattern was more convenient for complex resource cleanup.) And I’m not alone: I was just on a Go Time podcast panel where everyone else said roughly the same thing. While I understand your perspective that a one-line deferred error handler may seem like an easier sell—less typing, and less repetition—my experience tells me that I’d have a much harder time selling that sort of “look over here!” indirection as a general solution to novice programmers, versus short, in-situ repetition of
on errexpressions—of which we already have a reasonably effective (if more verbose) version inif err != nil.I believe the
tryproposal significantly under-values the costs of physically separating error decoration from the error-generating statements, and of preventing many other kinds of error handling altogether. It seems to me that if you boil Go down to its barest essence, one of the properties that surely must remain is (waving my hands and trying to paraphrase a lot of stuff here) that errors are not exceptional, that the “sad path” of code is at least as important as the “happy path”, and that the control flow from error handling should not be treated any differently from the control flow of business logic. It seems pretty clear to me thattrysubverts this essential property, and thaton errdoes not. And if, as @ianlancetaylor says,Then let us not change the language.
I support this proposal.
It is important that any change to error handling allows each error in a function to be annotated explicitly and independently from other errors. Returning the naked error, or using the same annotation for every error return in a function, may be useful in some exceptional circumstances, but are not good general practices.
I also support using a keyword instead of a builtin. If we add a new way to exit a function, it is important that it isn’t nestable within other expressions. If we have to wait until further improvements to the language allow this to be accomplished without breaking the Go 1 compatibility promise, then we should do that, rather than make the change now.
@griesemer
To be clear, I’m in the “leave if err != nil alone” camp, like many other veterans of the language. But it seems like the core team has decided this is a “problem” that’s worth solving, and I have sympathy for the perspective that it might encourage more not-yet-Gophers to become Gophers, so I’m speaking from that assumption.
edit: From that assumption, I think it’s important that any proposal not affect the semantics (for lack of a better word) of error handling as it exists today (by which I mean that errors are generally totally handled adjacent to the error-causing expression) and instead affect only the verbosity of error handling. And the only commonality between the error handling blocks I write which could be reduced are
For that reason, I feel like
onis about as good as it can get, I guess.I think this is one of many points that demonstrate a disconnect between the core Go team and Go as it is used in practice, at least as I have always experienced it. Over the years I’ve taught dozens Go courses, consulted for nearly a hundred companies, small and large, startups and Fortune-500; I suspect I’ve interacted with thousands of Go programmers, and seen nearly as many discrete Go codebases. They are almost exclusively private, and their developers infrequently respond to surveys. As I see it, this is the primary market for Go the language. (Of course, I may be wrong. Grain of salt and all.)
In these contexts there are almost no packages which are imported by more than one binary. In fact I’d estimate that ~50% of Go code I’ve seen has just been in
package main. These developers rarely have enough time or experience to properly structure the main business logic of their programs, much less think deeply about the structure or cleanliness of their errors. Many, many developers have the instinct to write functions like this, and use them everywhere:So, given this reality, where package boundaries are rarely well-defined, where functions instead serve as the primary way to hew borders in programs, the risk is never over-decoration. I’ve never once given a code review where I requested the author to de-annotate an error because it was too noisy. But I’ve given plenty of reviews where I’ve had to convince the author to remove their checkErr helper or equivalent and actually return errors at all rather than just terminating the process from somewhere deep in the callstack. So if a new language feature provides most of the “I don’t have to think about this” convenience of checkErr and requires something like a deferred block to do annotation, I’m positive that almost no developer as I’ve described them will perform any annotation whatsoever, and I fear we’ll move the resiliency and coherence of most Go codebases backwards, not forwards.
Of course,
on errallows this to happen, too, but at least in code review we can suggest in-line amendments, which I believe would be more eagerly adopted.@peterbourgon Thanks for your response.
It has become clear from much of this discussion that many are disappointed that
trydoesn’t do more for error handling. Maybe it’s not emphasized enough, but the proposal is clear that if decorating/handling an error at the point of the error check is desired, the existingif err != nilpattern is the recommended approach. We are at this point not convinced that there is anything else to do in that case (but see below).But there is the legitimate use case where errors are simply returned. And there is the legitimate use case where all errors returned from an API entry point are decorated uniformly (the same way) - which is where the deferred handler is suggested (which is independent and orthogonal to
try). We hear that these cases may not be the majority of cases, but they are not negligible, and they are the cases where the boilerplate becomes egregious because it adds only clutter. In those cases I would want to factor out that code, like one would with everything else that’s repeated and the same, but because we can’t do this in Go (and won’t be able to do this in any foreseeable future version of Go), the helper functiontryseems like a good compromise. It’s truly a minor addition to the language. If it doesn’t fit a specific program design, don’t use it.I think there’s a fundamental misunderstanding in this general discussion (not between you and I), a fear that
tryis the “must-use” mechanism for error handling, or perhaps more accurately, thattrymight become the “must-use” mechanism. That is not our intention, nor does the proposal say as much.Regarding the need for a more comprehensive error handling construct: philosophically, Go has never been in the business of promoting a specific programming paradigm (notwithstanding the fact that it is an imperative, statically typed, type-safe language). For instance, Go supports OO programming but doesn’t mandate it. Instead it provides all the building blocks (methods, interfaces, embedding) to achieve that. Go supports exception handling but doesn’t encourage programming with exceptions via a
try-catchconstruct. Instead it provides the building blocks (panic,defer, etc.) to do so if desired. That is not to say that Go is not opinionated - of course it is. But that opinion is not enforced so much by the language but by convention and by tools (go fmt,go vet,go lint, etc.).Whether the language or the tools enforce a paradigm doesn’t make much of a difference for a specific piece of software at a given time, the outcome is the same: a certain style is encouraged or enforced. But it matters in the long run: conventions can evolve as we learn more, and tools can change and adapt. Existing programs will continue to run. On the other hand, if we enshrine paradigms into the language it will become much harder or impossible to change them because it might invalidate a lot of existing code.
For that reason I am much more reluctant to make more significant language changes such as adding “on err” or the like (*).
tryis simply factoring code.on erris adding a new paradigm.At this point, I think the question we need to answer is this: Is it reasonable to add a minor built-in to the language that facilitates error checking for a significant number of cases (but not most), and which can be safely ignored if one desires to do so, or is even that not justified and we should leave the language alone.
Thanks.
(*) Adding the predeclared type
errorwas of course the first (and perhaps the biggest) step in this direction: It enshrined that errors should be oferrortype. But by making them more abstract (aserroris an interface type) it increased interoperability between components while introducing a standard, yet errors remain “just values”. This was clearly a win-win.As you hinted at yourself, the proposed syntax doesn’t gain much over just putting the equivalent
ifstatement in one line:I don’t think it’s worth making a breaking change to the language for something you can almost get with a
gofmttweak.@campoy
tryis just a macro, fashioned into a built-in function. If we could writetryin Go, one might write it and use it like any other function we use to factor out shared code (one sort of can, using ugly hacks withpanicanddefer, for specialized calls, but that’s beside the point).trydoesn’t introduce new syntax or new keywords; and since it lives in the universe scope, it can be shadowed like any other name. As a consequence, it won’t affect any existing code.on erris a new statement and requires a new keyword. It will interfere with any code that usesonas an identifier. It also overlaps directly with theifstatement, something we try to avoid for major language constructs. The syntaxon err, <statement>is unusual for Go, one would expecton err { ... }. I don’t know why there needs to be a comma. Details.Of course we could say that
on erris simply a form of syntactic sugar for a common pattern, very much liketry. And that if one could writeon errin Go some would (like some would withtry).But there is really a difference between a built-in function and a new statement. Built-ins can easily be ignored (because they don’t interfere with existing code). With few exceptions (
len,append), they can be skipped when learning the language, and introduced on demand. In the spec they show up at the end. Several built-ins are not strictly “needed” and one could write the same code by hand (append,copy,newcome to mind) - these are here purely for convenience.tryfalls into this category.A statement cannot be ignored - it is either used or it must be avoided (in the sense that we cannot have identifiers that collide with keywords of statements). It must be learned by everybody for that reason. In the spec it shows up prominently.
Generally, a statement in Go doesn’t enshrine a single use case: An
ifstatement is simply for conditional branching, aforstatement for looping, aswitchstatement for selecting between many alternatives, and so on. They operate on arbitrary data types and in different kinds of coding styles.But an
on errstatement is only for checking and handling errors. There’s no other use case. As such it will become the recommended way of checking errors, even though there is absolutely nothing wrong withif err != nil {}. Soon err- by sheer existence - promotes a certain coding style, or what I called a paradigm (maybe paradigm is not the best word).Not only does
on erroverlap withif, it also does not make code a lot more compact. Code that is now dominated byif err != nilwill become dominated withon err- there’s not much gained, and a lot lost (introducing a new language statement comes at a big cost).tryalso has a very narrow use case. It’s only good for one thing: checking an error and return. But it doesn’t affect existing code. Because it’s much more narrow thanon err, the savings are much bigger. It’s also more powerful thanon errbecause it can be used in an expression, something we can’t do withon err. Yet it comes at much less cost. It doesn’t scream “you must use me for error handling” in the language spec. It’s just a little built-in for convenience (see above). It terms of complexity of implementation it is trivial compared to sayappend,copy, etc. It is tremendously helpful in many situations, very much likeappendis, but it doesn’t have to be used.One more thing, unrelated to your question: We (the Go team) have considered many many alternatives (some of which I mentioned in the design doc). But for minor syntax differences, most alternative proposals brought up in this public discussion we have already considered one way or the other (we have been discussing error handling internally on a regular basis for more than a year). We ended up with
trybecause it seemed to have the least negative impact on existing code and the language, and because it seemed easy to explain and very clear. Where it can be used it has a big impact on the readability of the code (in our minds) because it reduces so much boilerplate. It opens new avenues of use because it can be used in expressions where before we needed separate statements. The same cannot be said for most alternative proposals that have been brought up so far (but the ideas that make error checking invisible and implicit - nonstarters). But it does require getting used to. It’s a bit of a shift in thinking about errors. It only solves error checking, not error handling. It takes time. And maybe the time is not now, and maybetryis not it. We don’t know without try-ing 😃Apologies for the lengthy reply (I didn’t take enough time to make it shorter). I hope this helps explaining a bit better what I tried to say here.
I’m not sure what kind of help you are looking for from the Go team, or what further refinements are appropriate. You’ve gotten comments from at least three members of the Go team here.
As I suggested above, I personally am not in favor of this, as it adds a new keyword, and a new kind of statement syntax, and a new kind of expression, all to save the boilerplate of a comparison expression. Saving boilerplate is good but adding three new ideas to the language is a heavy cost.
To be very clear, I am not trying to say “therefore we should implement the
tryproposal.” I am saying that this proposal, viewed by itself, does not, in my personal opinion, provide a benefit that exceeds the cost. And I want to stress that the cost of a new kind of statement syntax and a new kind of expression is a high cost for a language like Go that aims to be orthogonal and to have a minimal number of concepts. And, again, to be very clear, it is perfectly fair to argue that thetryproposal is also not orthogonal, but it does not follow that we should adopt this proposal.Hey @griesemer, regarding the line
I’m curious about how is
trysimply factoring code when it’s able to make the function return? Not criticizing, just trying to figure out whytryis seen as something “simple”, whileon erris a new paradigm.@ianlancetaylor
For me personally with
trythere are none savings only more mud. I am voting for the status quo, but if we’re thinking changes lets think about what a possible outcome of stepping back to Cifexpression semantics would be. Being implicit it might even tackle at immortalif interface == nilgotcha.PS. (Edit)
But wasn’t this given as a rationale for the
tryproposal? “On popular demand…”.(I remember your rebuke to mine (at go-newbie times) proposal of iirc
watchsome years ago.**Spooky action at distance**. I was then wrong, you were right.Trywill allow for the spooky action not only at distance but also well hidden in layers of wrapped calls.)If the members of the Go team have personal objections to the
tryproposal, they will state them.I’m discussing this because you explicitly said, in https://github.com/golang/go/issues/32611#issuecomment-508959846
I felt that that statement was unfair, given that at least three members of the Go team had already commented on the proposal, and given that we try very hard to stay open to input at all times.
Also, you brought up the proposal on golang-nuts saying that it may have traction, although the downvotes appear to outnumber the upvotes at least at this time.
I see serious problems with this specific proposal, as I’ve expressed above. I’ve tried to point out problems I see in a number of other proposals as well.
That turns out not to be the case. I don’t think we could possibly keep up with the discussion if we had to get together and formulate an agreed-upon response before each reply. We’re not even all in the same timezone. And I’m sure there have been cases where we have publicly disagreed with each other.
@networkimprov
With all due respect, all that additions and refinements morph your initial simple solution into something that will need no less cognitive load as
try. IMO.@griesemer
But yours
try, while smart, makes writing convoluted paths convenient, using magic that (except autodereferencing) so far has not been used in Go spec.ifisif, boolean expressions are 101, then an apprentice will need to learn and remember that:[note that this is a description of reasoning, not a manual entry]
try, while looks like a function call, is in fact not atryfunction call, but it is an invocation of a magic ~spell~ macro that will behave like a function call to the function given intry’s parenthesis, if and only if that real call will return nil in its last return parameter of typeerrorposition. Otherwise, as for the last return parameter of called func not beingnil,trymagically will morph into a plainnaked return…never forget that the outermost, and outermost only,tryincantation cuts off the last return parameter (usually err)! .Tryin its current shape adds too much convenience for too hefty price tag. Readability and simplicity-wise. Make it a statement at least, please, so it will have to occupy its own line. It is thetry’s ability to nest that concerns. Frankly, I know I personally would be tempted to abuse it, then curse on myself later.I don’t really understand this point. From the perspective of a language user, there is no difference between a keyword and a builtin, except the way in which it is used. In either case, when they are learned is a function of their general applicability; it’s never been my experience that e.g. builtins are more likely to be learned later or ignored, as I believe has been implied.
More concretely, whether the error handling feature is a keyword or a builtin, it would certainly appear in the same place in a Go learning syllabus. Right? Could someone clarify?
@lootch Thanks for your comment. There’s definitively a social engineering aspect to all of this - we’re proposing to extend Go in unfamiliar ways, and if it’s the language you use day in and day out that is very concerning, perhaps even scary; after all it changes how one is supposed to “speak” in this language. You are absolutely right that there are a lot of emotions running through this discussion - I can’t see how a purely technical exchange would collect so much feedback in so little time. As to the decision, it’s still early days. I’m not the only decision maker here but I suspect we will stick to the process and let this run its course and decide how to proceed by the end of July.
Regarding the insurmountable issues with
try: not to be facetious, but it seems to me that the existence oftryis proof that these issues are not insurmountable 😃. Yes,tryhas odd requirements and behavior. The design doc is explicit on this subject. I note thatnewandmakerequire a first argument which is a type, followed by regular arguments. No Go function has such a signature.unsafe.Offsetofrequires a struct field (!) as argument - talking about an unusual signature.recoveris only meaningful inside a deferred function and possibly stops an ongoing return/panic - another odd context-dependent requirement and strange control-flow behavior. Virtually every single existing built-in has some “code smell”, which is the very reason they are built-ins.Regarding the claim that the
trydesign flies in the face of Go’s philosophy or that it is an aesthetic failure: Since the proposal passed muster with the Go team, including several of the original Go designers before we published it, I’m going to make the bold statement that it is ok. As long as Go has agotostatement I think we can’t be feeling too smug about Go’s aesthetics.But let me repeat what I’ve said elsewhere:
You already have a loud & clear answer to that question from the community.
Re “minor” built-in, please consider whether that’s a rationalization.
The bet here is that
on erruse at almost all call sites that yield an error delivers far greater benefit for essentially the same cost. Actually lower cost, since control-flow cannot become buried, and integration into existing code is trivial.If data from a revised tryhard bears that out, what argument would remain for
try()?I was just thinking how inlining a handler would look like.
The comma doesn’t feel right in that case. This makes me think if using curly brackets instead of the comma is an option. However, the code then becomes more like the boilerplate that we have already with
if.I think option 3. from networkimprov has been underestimated (to quote): “go fmt could just allow single-line if statements: Would it also allow single-line case, for, else, var () ? I’d like them all, please 😉 The Go team has turned aside many requests for single-line error checks.”
If gofmt were to allow single-statement if-statements for err != nil to appear on a single line, visual clutter would be greatly reduced, without any change to the Go language.
I come from C and I greatly value the enormous effort and experience that has gone into making Go simple to understand and use. I also appreciate gofmt which removes a lot of trivial formatting decision making.
A 3-line boilerplate handball up the stack of an error from a called function is not the end of the world for me or anyone else.
But … in libraries I wrote in C, I returned an error value from almost every function and handballed it up the stack with a single line if statement.
Where an error needed to be originated, or a returned error needed to be decorated, I did that on multiple lines.
There was no error handling clutter, except where it counted: new error generation, and decoration.
Error generation, decoration and handballing was clear and uncluttered. And that was achieved by the C equivalent of:
if err != nil { return err }I guess I’m not sure where that comes from. Personally, I think that we should try to do something about error handling, given how often it comes up as an issue for people learning and using Go. When people makes jokes about a language pattern (e.g., https://images.app.goo.gl/khYRQQUSYaGCEiwV9) they are pointing to a deficiency in the language.
Note that
elsecan not appear as a statement by itself. You can’t even writeAn
elseclause is intrinsically part of anifstatement. So ifcatchis likeelse, it seems to me that there should be some direct syntactic connection between the assignment statement and thecatchclause. I’m not sure I like that either, but I think it would be better than a standalone statement that is not really standalone.The same applies to your
defaultexample; it is syntactically associated with aswitchstatement.The restriction on writing
func f() {}inside a function seems to me like a different kind of thing. It is not an example of something added, but rather of something prohibited. (I think the reason we don’t permitfunc f() {}inside a function is that we could never decide on the scope of the identifierf: is it visible inside the body off? Is it visible to other nested functions that appear beforef?)Both examples are testing an implicitly defined variable
erragainst a non-nil value. If there an implicit!= zerotest, I’m not sure where it applies. But, even accepting that, that is just saving a few characters over anifstatement.I only occasionally speak for the Go team, but the fact that I occasionally do is why in a comment like this I am careful to explicitly state that I am speaking only for myself. I can’t control whether people absorb that, the best I can do is try to be clear and honest.
Here again, I am speaking only for myself.
@networkimprov For the record, I do not accept your distinction between “cost to users” or “cost to the ecosystem” and “cost to the spec.” I do not find that to be a useful line to draw.
As I’ve said multiple times in this issue, my comments above are specifically about this proposal. You keep bringing up the
tryproposal, but my comments above are not about thetryproposal. I’m honestly not sure, but I think you are making an argument that is something like 1) we must do something about errors; 2) thetryproposal is bad; 3) this proposal is better than thetryproposal; 4) therefore, we should accept this proposal. If that is the argument you are making, I don’t agree with it.I would say, instead, that I do not think this proposal is a good idea, and I do not think we should adopt it. I’ve explained my reasoning above.
We can then, separately, discuss the
tryproposal. It may very well be that thetryproposal is bad and that we should not accept it. But even if we agree on that, even if we agree that we should not accept thetryproposal, I do not think that we should therefore adopt this proposal.If you reply to this comment, I encourage you to make an effort to not mention the
tryproposal. It simply isn’t relevant to the discussion of this proposal.With regard to
catchrather thanon err:catch <stmt>can apparently only appear after an assignment statement, a form of restriction that applies to no other statement in Go. It apparently implicitly refers to a variable set or defined in the previous statement, a form of implicit reference that occurs in no other statement in Go.catchappear to me to be identical toif, except that the curly braces are omitted. That seems fairly minor.I am speaking only for myself here.
@ianlancetaylor, I’m afraid you’ve missed my point. The community is at the barricades about
try()because we believe it would damage our code and/or the ecosystem of code we depend on, and thereby reduce our productivity and quality of product. That’s the “cost to users” I refer to. Perhaps I should call it the “cost to the ecosystem.”This proposal does no such damage.
The “cost to the spec” as I define it includes the need to “explain to all users of Go” that a feature adds unique concepts to the language. As I’ve stated, the cost to the spec should be a far smaller concern than the cost to the ecosystem.
As this proposal has virtually no cost to the ecosystem, and roughly equal cost to the spec as
try(), there’s really no contest.Re “this is what an expression looks like, except … in an
on errstatement,” I addressed that withcatch <stmt>in Critiques no. 1.@peterbourgon I think it’s fair to say that a novice to Go will be able to program without
tryand do error handling - it’s not something that needs to be learned to get going (it’s what we do now, after all). But you’re right that once they start reading other people’s code, they will have to look it up. And yes, iftrybecomes common practice, it probably will be introduced in a syllabus right after explaining how to useif err != nil.(We have a few built-ins that we don’t strictly need:
newcan be done by taking the address of a variable,appendandcopycould be custom-written,complexI’ve already mentioned, andprintandprintlnare for convenience.tryfalls into a similar category in my mind; it adds convenience where it is applicable. I haven’t heard the complaint that the presence of these “unnecessary” built-ins has made Go harder to learn due to the increased complexity.)@ianlancetaylor there’s nothing like
try()(either its arguments or effects) in the language now either, so apparently it’s ok to devise novel schemes to reduce error handling boilerplate 😃What
on erroffers thattry()lacks is application to virtually all errors, including recoverable ones.Sure, the concept needs work to support a variety of use cases in a way that blends with the rest of Go.
I think the boilerplate that counts is what people see.
This reduces three lines to one line.
It reduces
if err != nil {}toon err,, which by my count is reducing 12 characters to 6.This adds a new kind of constrained expression which does not exist elsewhere in the language. The suggested extensions seem to mostly try to unconstrain the expression a bit by writing
||as,.To me personally I don’t think the savings are enough to justify the language change.
Forgive me if this is the wrong place for this feedback.
I guess this may be getting at what Robert wrote in the closing comment to the try proposal, when he noted that it’s worth clarifying what the problem we’re trying to solve actually is. But I challenge this summary: in my opinion it is precisely and only the boilerplate that should be reduced, the current semantics—error handling as separate statements/blocks directly adjacent to the error-generating code—are important and foundational and should not be changed.
edit: Some questions that we should probably come to a rough consensus on as a community, before spending too much time on future proposals, include, in my opinion:
@networkimprov
You provide no evidence for any of these assertions.
The last one, in particular, makes no sense to me.
tryis a single built-in function which can be described straightforwardly in terms ofifandreturn, and requires no changes to the language grammar. This proposal, on the other hand, introduces two new statements, one of which,on err, has a somewhat novel syntax for Go, as well as a new predeclared typecaller. It gets even more complex if one incorporates some of the possible extensions you also propose.@networkimprov Thanks for the comment. However, I don’t agree that it is possible to separate cost to the spec and cost to users. If the spec is more complicated, then it is harder for people to learn the language, and there is more for readers and writers to remember. Simplicity is a significant goal of Go, based on the assumption that keeping the spec simple means keeping the language simple means keeping it simple for programmers to learn and to use. If
on errrequires a different kind of expression, then we have to explain to all users of Go “this is what an expression looks like, except that you have to remember that in anon errstatement there are various restrictions.” That is a cost to the spec and it is also a cost to users.And, again, I am not talking about
try. I am talking abouton err. Each proposal must stand and fall on its own. It does not make sense to say “tryis bad therefore we should adopton err.” Perhapstryis bad, in which case we should not adopt it. But we shouldn’t take the arguments abouttryand use them to say that we should adopton err. They are two separate and independent proposals. The only connection between them is that it is very unlikely that we would adopt both.@griesemer
Then why not
onErrmacro instead? One that only checks for err != nil, then if true executesreturn,gotoorpanic?It will badly affect all future code
[…]
No they can NOT be ignored while reading and comprehending the code. (And I personally am reading my own code ten times more often than I write it. Not to mention other’s code I am trying to dissect.)
IMO, only the
complex,realandimagcan be introduced on demand. All the rest must be understood, at least for their purpose and result, while onboarding into a project. How one could be supposed to read Go code without knowing of signature of themake, ehm?No,
trydoes not fall into that category. Neitherappend, norcopy, normakehave peculiar demands about other parts of the code nor they need augmenting prose in the specs.And thats good. We do expect all apprentices to learn that failures must be dealt with in clear.
[…]
Having
tryas a function allows for, if not encourages to, hard to notice slips (pointed at #32437 discussion). I can add to this: how a debugger output will look like to set my attention at a fifth wrap of thetryies oneliner matrioshka? And how much effort will it demand from debugger authors?An the false (write time only) convenience of it will make it hard to eradicate from the Go newcomers minds, so we will soon see a 20 cases switch at the topmost defer then down the body single lines dense of nesting try-parenthesis soup. LISPers will be happy, Gophers will not.
onErr as a built-in macro will be yet more trivial to implement without endangering Go’s purity:
(excuse me Lucio, its yours “onerr” words and my simplified version. We all are in quest for the most gain from the least changes set 😉
(edit: added
continueandbreakallowed statements. I’d like to have alsofallthroughbut its likely too much demand compiler-changes wise)I don’t get the point of “but if we have on line if-statements”. We do now have them, and AFAIK the go team doesn’t want them. We don’t have them for a reason. If we add them that would pretty much nullify this proposal, but that doesn’t mean that this proposal is any worse because of it. this has a clear advantage over on-line if-statements, it is very clear what is going to happen, and is less general then an if-statement. You cant just use this wherever.
You’re right of course that costs and benefits are somewhat subjective. As has been said, Go is an opinionated language.
That said I think that you are simply mistaken in the conclusions that you are drawing. I think it’s clear that Go has considerable tolerance for boilerplate. I don’t see how it’s possible to conclude that the Go team would only accept a proposal that reduces boilerplate down to zero lines.
I agree that additional syntax on the left hand side of an assignment statement is a bigger lift, since there is nothing like that in Go today. Currently the left hand side of an assignment must be addressable, be a map index expression, or be the special identifier
_. In those three cases assignment is a fairly straightforward operation: just copy or discard a value. To change the left hand side of an assignment statement to cause some action other than assignment to occur is a big cost and requires a big benefit.But to say that any of these ideas are ruled out a priori is to misunderstand the nature of costs and benefits. Even if costs and benefits are subjective, which to some extent they are, it doesn’t mean that the costs can not be incurred. It just means that they need a corresponding benefit.
Clearly there are some changes that will never be made to Go. For example, we will never break significant amounts of existing Go code; that is a cost that is so high that it’s impossible to imagine a corresponding benefit. But I don’t see why the kinds of changes we are talking about here rise to that level.
I could very well be wrong, but it seems to me that in these discussions you persistently underestimate the costs of a language change. That underestimation, if real, may lead you to conclude that the Go team must not see the benefits of the change. I encourage you to seriously consider that perhaps we do see at least some of the benefits, but that we judge the costs differently than you do.
Above you tried to separate cost to the spec, cost to the users, and cost to the tooling. That is very different from the way that I view the costs of language changes. The only cost that matters is the cost of understanding the language. Every special case in the language (and the language has far too many special cases already) is a cost. The goal is a language that is simple and orthogonal. Every concept in the language should ideally interact with every other concept clearly and simply. When certain kinds of expressions that can only be used in one place, when we add certain kinds of statement syntax that can only be used with one statement, those are big costs.
As you know, the discussion of the
tryproposal stressed that it was a builtin function that changed no other aspect of the language. That is another way of saying that cost oftrywas limited to understanding one more special case, a special case akin to the existing special cases of the builtin functionspanicandrecover. There was no new syntax, no new constraints, no affect on general language orthogonality. Those were important points in limiting the cost oftry. Of course it still had costs, and the heated discussion was in itself a significant cost, and in the end we agreed that those costs were too high for the benefit oftry.Speaking purely personally, I don’t agree with @networkimprov 's comment at https://github.com/golang/go/issues/32611#issuecomment-512933966 . We don’t have specific requirements of that sort. If we did, we would say them.
We do our best to judge each proposal on its own merits. Every language change has costs and benefits. I want to stress, again, that this specific proposal introduced new concepts into the language, and that is a cost. We need a benefit that is worth that cost, and in the opinion of the proposal review team the benefit of this specific proposal was not worth the cost of this specific proposal.
To conclude from that that the Go team is only interested in “zero lines of boilerplate” or “no changes to assignment LHS” is to misunderstand what we are saying. We are weighing costs and benefits. We are not saying that only certain kinds of changes are acceptable. We are saying that the benefits of a change must outweigh the costs of a change.
This should be confirmed by the Go team and not merely assumed. (This is also what I was driving at with my set of scoping questions here.)
Hi @peterbourgon, some quick comments.
This current proposal is fairly broad, including the proposal itself has a fair number of permutations
Sometimes, “broader” is good because it means the solution is more general. In other cases, “broader” increases the cost of a change, or increases overlap with existing alternatives.
For example, as originally written, as far as I followed, any zero value is supported and it is not specific to error types, which would mean something like this is supported under the original proposal here:
In that case, that is a broader capability than strictly targeting error handling (which in turn requires a judgement call as to whether or not it is good to be broader in that way).
I think there is a narrower form of this proposal that is closer to what I think I might have seen you sketch roughly as:
In any event, because this proposal has a fair number of permutations, in some cases it has been hard at least for me to understand which permutation a specific comment is addressing.
(Finally, I am speaking for myself as a more-or-less random gopher from the broader community. In other words, just to be explicit, I am not member of the core Go team).
@ohir Concerns around nesting is not new feedback, and again, a
go/analysischeck for nestedtrys or multipletrys on the same line seems entirely straightforward. And I suspect there would be support for getting it intovetas a default check.@networkimprov I think this proposal needs to be specific about what exactly it’s proposing. The proposal seems to have a wide range of possible changes to the language, and it’s making it hard to evaluate. The specific keyword doesn’t really matter at all, but the exact semantics of what you’re proposing does. For instance, any thought like “is there implicit dataflow between an unnamed variable assignment and the following statement?” always has an answer of “maybe”, as there are some things in your proposal that imply yes and others that imply no. Same for a foundational question like “does the keyword only apply to values of type error?”.
This! It’s precisely the same temptation as the ternary operator proposals that the Go team has repeatedly quashed (rightly, imo.)
@griesemer, that is a valid set of points.
That said, when it comes to learning about a
trybuiltin vs. some statement-based variation, another set of questions is “how prominent would it be” and “how many words would it take to describe” in places like:Setting aside for the moment the exact form a statement-based variation of
trymight take, one could certainly imagine that there is a statement-based variation oftrythat would end up being equally prominent in those places, and it could take roughly the same number of words to usefully introduce the concept and mechanism as atrybuiltin.In other words, I suspect a builtin-based
trywould in practice end up being learned more-or-less just as soon for a new gopher as a statement-basedtryvariation (given error handling is fairly central to how one writes Go), and one could imagine there exists a statement-based syntax that could be learned roughly as easily.On your points about the spec – one of the things people praise about Go is how the spec is very readable, and seasoned gophers often recommend new gophers take the time to read the spec… but even there, a “typical” new gopher likely reads several other introductory things prior to venturing into the spec.
In any event, thanks for taking the time to share your thinking as well as to engage on the different concerns and ideas that have surfaced from the broader community since the proposal was published.
@ngrilly
This
on errproposal reduces it to one, which I think is the Pareto optimal number of lines, for the reasons I’ve discussed. Can noton errbe the improvedtry?Thanks, @peterbourgon for your detailed comment. We’ve picked up error checking because the community has repeatedly and strongly complained about the verbosity of error handling. And yes, we also think that it might be nice if there were a less verbose approach where possible. So we’re trying to move this forward if we can.
From what you’re saying,
trywould already be a big step up fromcheckErr- for one it takes care of dealing with an error in the first place, and it also promotes at least an error return rather than just a panic. Because it removes the need for acheckErrfunction and extra call on a separate line, I’d expect thosecheckErrprogrammers to flock totry.I see your point that
on errmight be easier to change into proper error handling (i.e., decoration), but again, judging from the reality you are describing it seems thatcheckErrwould still be the much easier way out. If these programmers have rarely enough time or experience to do error handling “properly”, why not provide a tool that gives them a strong incentive to check and forward errors? And rather than insist on decorating each error separately, use a deferred error handler (which can be one line)? This seems like a much easier sell than having them replacecheckErrwith anon errstatement.(As an aside, I’ve written plenty of little tools that just panic when I don’t care about the error - not great, but good enough when time is of the essence.
trywould make my life much easier and allow me to do something better than panic.)