golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.28k stars 17.57k forks source link

proposal: spec: add built-in result type (like Rust, OCaml) #19991

Closed tarcieri closed 4 years ago

tarcieri commented 7 years ago

This is a proposal to add a Result Type to Go. Result types typically contain either a returned value or an error, and could provide first-class encapsulation of the common (value, err) pattern ubiquitous throughout Go programs.

My apologies if something like this has been submitted before, but hopefully this is a fairly comprehensive writeup of the idea.

Background

Some background on this idea can be found in the post Error Handling in Go, although where that post suggests the implementation leverage generics, I will propose that it doesn't have to, and that in fact result types could (with some care) be retrofitted to Go both without adding generics and without making any breaking changes to the language itself.

That said, I am self-applying the "Go 2" label not because this is a breaking change, but because I expect it will be controversial and, to some degree, going against the grain of the language.

The Rust Result type provides some precedent. A similar idea can be found in many functional languages, including Haskell's Either, OCaml's result, and Scala's Either. Rust manages errors quite similarly to Go: errors are just values, bubbling them up is handled at each call site as opposed to the spooky-action-at-a-distance of exceptions using non-local jumps, and some work may be needed to convert error types or wrap errors into error-chains.

Where Rust uses sum types (see Go 2 sum types proposal) and generics to implement result types, as a special case core language feature I think a Go result type doesn't need either, and can simply leverage special case compiler magic. This would involve special syntax and special AST nodes much like Go's collection types presently use.

Goals

I believe the addition of a Result Type to Go could have the following positive outcomes:

  1. Reduce error handling boilerplate: this is an extremely common complaint about Go. The if err != nil { return nil, err } "pattern" (or minor variations thereof) can be seen everywhere in Go programs. This boilerplate adds no value and only serves to make programs much longer.
  2. Allows the compiler to reason about results: in Rust, unconsumed results issue a warning. Though there are linting tools for Go to accomplish the same thing, I think it'd be much more valuable for this to be a first-class feature of the compiler. It's also a reasonably simple one to implement and shouldn't adversely affect compiler performance.
  3. Error handling combinators (this is the part I feel goes against the grain of the language): If there were a type for results, it could support a number of methods for handling, transforming, and consuming results. I'll admit this approach comes with a bit of a learning curve, and as such can negatively impact the clarity of programs for people who are unfamiliar with combinator idioms. Though personally I love combinators for error handling, I can definitely see how culturally they may be a bad fit for Go.

Syntax Examples

First a quick note: please don't let the idea get too mired in syntax. Syntax is a very easy thing to bikeshed, and I don't think any of these examples serve as the One True Syntax, which is why I'm giving several alternatives.

Instead I'd prefer people pay attention to the general "shape" of the problem, and only look at these examples to better understand the idea.

Result type signature

Simplest thing that works: just add "result" in front of the return value tuple:

func f1(arg int) result(int, error) {

More typical is a "generic" syntax, but this should probably be reserved for if/when Go actually adds generics (a result type feature could be adapted to leverage them if that ever happened):

func f1(arg int) result<int, error> {

When returning results, we'll need a syntax to wrap values or errors in a result type. This could just be a method invocation:

return result.Ok(value)
return result.Err(error)

If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".

Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):

return Ok(value)
return Err(value)

Propagating errors

Rust recently added a ? operator for propagating errors (see Rust RFC 243). A similar syntax could enable replacing if err != nil { return _, err } boilerplate with a shorthand syntax that bubbles the error up the stack.

Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.

First, an example with present-day Go syntax:

count, err = fd.Write(bytes)
if err != nil {
    return nil, err
}

Now with a new syntax that consumes a result and bubbles the error up the stack for you. Please keep in mind these examples are only for illustrative purposes:

count := fd.Write!(bytes)
count := fd.Write(bytes)!
count := fd.Write?(bytes)
count := fd.Write(bytes)?
count := try(fd.Write(bytes))

NOTE: Rust previously supported the latter, but has generally moved away from it as it isn't chainable.

In all of my subsequent examples, I'll be using this syntax, but please note it's just an example, may be ambiguous or have other issues, and I'm certainly not married to it:

count := fd.Write(bytes)!

Backwards compatibility

The syntax proposals all use a result keyword for identifying the type. I believe (but am certainly not certain) that shadowing rules could be developed that would allow existing code using "result" for e.g. a variable name to continue to function as-is without issue.

Ideally it should be possible to "upgrade" existing code to use result types in a completely seamless manner. To do this, we can allow results to be consumed as a 2-tuple, i.e. given:

func f1(arg int) result(int, error) {

It should be possible to consume it either as:

result := f1(42)

or:

(value, err) := f1(42)

That is to say, if the compiler sees an assignment from result(T, E) to (T, E), it should automatically coerce. This should allow functions to seamlessly switch to using result types.

Combinators

Commonly error handling will be a lot more involved than if err != nil { return _, err }. This proposal would be woefully incomplete if that were the only case it helped with.

Result types are known for being something of a swiss knife of error handling in functional languages due to the "combinators" they support. Really these combinators are just a set of methods which allow us to transform and selectively behave based on a result type, typically in "combination" with a closure.

Then(): chain together function calls that return the same result type

Let's say we had some code that looks like this:

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

With a result type, we can create a function that takes a closure as a parameter and only calls the closure if the result was successful, otherwise short circuiting and returning itself it it represents an error. We'll call this function Then (it's described this way in the Error Handling in Go) blog post, and known as and_then in Rust). With a function like this, we can rewrite the above example as something like:

result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
    return result.Error()
}

or using one of the proposed syntaxes from above (I'll pick ! as the magic operator):

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

This reduces the 12 lines of code in our original example down to three, and leaves us with the final value we're actually after and the result type itself gone from the picture. We never even had to give the result type a name in this case.

Now granted, the closure syntax in that case feels a little unwieldy/JavaScript-ish. It could probably benefit from a more lightweight closure syntax. I'd personally love something like this:

final_value := doThing(a).
    Then(|resp| doAnotherThing(b, resp.foo())).
    Then(|resp| FinishUp(c, resp.bar()))!

...but something like that probably deserves a separate proposal.

Map() and MapErr(): convert between success and error values

Often times when doing the if err != nil { return nil, err } dance you'll want to actually do some handling of the error or transform it to a different type. Something like this:

resp, err := doThing(a)
if err != nil {
    return nil, myerror.Wrap(err)
}

In this case, we can accomplish the same thing using MapErr() (I'll again use ! syntax to return the error):

resp := doThing(a).
    MapErr(func(err) { myerror.Wrap(err) })!

Map does the same thing, just transforming the success value instead of the error.

And more!

There are many more combinators than the ones I have shown here, but I believe these are the most interesting. For a better idea of what a fully-featured result type looks like, I'd suggest checking out Rust's:

https://doc.rust-lang.org/std/result/enum.Result.html

bradfitz commented 7 years ago

Language change proposals are not currently being considered during the proposal review process, as the Go 1.x language is frozen (and this is Go2, as you've noted). Just letting you know to not expect a decision on this anytime soon.

ghost commented 7 years ago
final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

I think this wouldn't be the right direction for Go. ()) })!, seriously? The main goal of Go should be ease of learning, readability and ease of use. This doesn't help.

hectorj commented 7 years ago

As someone said in the reddit thread: would definitely prefer proper sum types and generics rather than new special builtins.

tarcieri commented 7 years ago

Perhaps I was unclear in the post: I would certainly prefer a result type be composed from sum types and generics.

I was attempting to spec this in such a way that the addition of both (which I personally consider to be extremely unlikely) wouldn't be a blocker for adding this feature, and it could be added in such a way that, when available, this feature could switch to them (I even gave an example of what it would look like with a traditional generic syntax, and also linked to the Go sum type issue).

ianlancetaylor commented 7 years ago

I don't understand the connection between the result type and the goals. Your ideas about error propagation and combinators appear to work just as well with the current support for multiple result parameters.

tarcieri commented 7 years ago

@ianlancetaylor can you give an example of how to define a combinator that works generically on the current result tuples? If it's possible I'd be curious to see it, but I don't think it is (per this post)

ianlancetaylor commented 7 years ago

@tarcieri That post is significantly different, in that error does not appear in its suggested use of Result<A>. This issue, unlike the post, seems to be suggesting result<int, error>, which to me implies that the proposed combinators are specially recognizing error. My apologies if I misunderstand.

tarcieri commented 7 years ago

The intent is not to couple result to error, but for result to carry two values, similar to the Result type in Rust or Either in Haskell. In both languages, by convention the second value is usually an error type (although it doesn't have to be).

This issue, unlike the post, seems to be suggesting result<int, error>

The post suggests:

type Result<A> struct {
    // fields
}

func (r Result<A>) Value() A {…}
func (r Result<A>) Error() error {…}

...so, to the contrary, that post specializes around error, whereas this proposal accepts a user-specified type for the second value.

Admittedly things like result.Err() and result.MapErr() give a nod to this value always being an error.

griesemer commented 7 years ago

@tarcieri What's wrong with a struct? https://play.golang.org/p/mTqtaMbgIF

tarcieri commented 7 years ago

@griesemer as is covered in the Error Handling in Go post, that struct is not generic. You would have to define one for every single combination of success and error types you ever wanted to use.

griesemer commented 7 years ago

@tarcieri Understood. But if that (non-genericity, or perhaps not having a sum type) is the problem here, than we should address those issues instead. Handling result types only is just adding more special cases.

tarcieri commented 7 years ago

Whether or not Go has generics is orthogonal to whether a first-class result type is useful. It would make the implementation closer to something you implement yourself, but as covered in the proposal allowing the compiler to reason about it in a first class manner allows it e.g. to warn for unconsumed results. Having a single result type is also what makes the combinators in the proposal composable.

griesemer commented 7 years ago

@tarcieri Composition as you suggested would also be possible with a single result struct type.

spiritedsnowcat commented 7 years ago

I don't understand why you wouldn't use an embedded or defined struct type. Why have specialized methods and syntax for checking errors? Go already has means of doing all of this. It seems like this is just adding features that don't define the Go language, they define Rust. It would be a mistake to implement such changes.

tarcieri commented 7 years ago

I don't understand why you wouldn't use an embedded or defined struct type. Why have specialized methods and syntax for checking errors?

To repeat myself again: Because having a generic result type requires... generics. Go does not have generics. Short of Go getting generics, it needs special-case support from the language.

Perhaps you're suggesting something like this?

type Result struct {
    value interface{}
    err error
}

Yes, this "works"... at the cost of type safety. Now to consume any result we have to do a type assertion to make sure the interface{}-typed value is the one we're expecting. If not, it's now become a runtime error (as opposed to a compile time error as it is presently).

That would be a major regression over what Go has now.

For this feature to actually be useful, it needs to be type safe. Go's type system is not expressive enough to implement it in a type-safe manner without special-case language support. It would need generics at a minimum, and ideally sum types as well.

It seems like this is just adding features that don't define the Go language [...]. It would be a mistake to implement such changes.

I covered as much in the original proposal:

"I'll admit this approach comes with a bit of a learning curve, and as such can negatively impact the clarity of programs for people who are unfamiliar with combinator idioms. Though personally I love combinators for error handling, I can definitely see how culturally they may be a bad fit for Go."

I feel like I have confirmed my suspicions and that a feature like this both isn't easily understood by Go developers and goes against the simplicity-oriented nature of the language. It's leveraging programming paradigms that, quite clearly, Go developers don't seem to understand or want, and in such case seems like a misfeature.

they define Rust

Result types aren't a Rust-specific feature. They're found in many functional languages (e.g. Haskell's Either and OCaml's result). That said, introducing them into Go feels like a bridge too far.

as commented 7 years ago

Thank you for sharing your ideas, but I think the examples used above are unconvincing. To me, A is better than B:

A

if err != nil {
    return nil, err
}
if resp, err = doAnotherThing(b, resp.foo()); err != nil {
    return err
}
if resp, err = FinishUp(c, resp.bar()); err != nil {
    return err
}

B

result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
    return result.Error()
}
urandom commented 7 years ago

I don't think A is more readable. In fact, the actions aren't noticeable at all. Instead, the first glance reveals that a bunch of errors are being obtained and returned.

If B were to be formatted so that the closure bodies were on new lines, that would've been the most readable format.

Also, the last point seems a bit silly. If function call performance is so important, then by all means, go with a more traditional syntax.

iporsut commented 7 years ago

A From @as I think we normal flow should no indent.

if err != nil {
    return err
}

resp, err = doAnotherThing(b, resp.foo());
if  err != nil {
    return err
}

resp, err = FinishUp(c, resp.bar());
if  err != nil {
    return err
}
tarcieri commented 7 years ago

One interesting observation from this thread: the original example I gave which people keep copying and pasting contained some errors (the first if returned nil, err on error, the subsequent two only return err). These errors were not deliberate on my part, but I think it's an interesting case study.

Though this particular class of error is the sort that would've been caught by the Go compiler, I think it's interesting to note that how with so much syntactic boilerplate, it becomes very easy to look past such errors when copying and pasting.

as commented 7 years ago

This doesn't make the proposal better. It's an assumption that failing to return multiple values is a result of explicit error handling. You could have also made the same errors inside the functions, you just wouldn't have seen them due to their unnecessary encapsulation.

jredville commented 7 years ago

I disagree, I think that is a strong point of this kind of proposal. If all a program is doing is returning the err and not processing it, then it is wasting cognitive overhead and code and making things less readable. Adding a feature like this would mean that (in projects that to choose to use it) code that deals with errors is actually doing something worth understanding.

as commented 7 years ago

We will have to agree to disagree. The magic tokens in the proposal are easy to write, but difficult to understand. Just because we have made it shorter doesn't mean we've made it simpler.

creker commented 7 years ago

Making things less readable is subjective, so here's my opinion. All I see in this proposal is more complex and obscure code with magic functions and symbols (which are very easy to miss). And all they do is hide a very simple and easy to understand code in case A. For me, they don't add any value, don't shorten the code where it matters or simplify things. I don't see any value in treating them at a language level.

The only problem that the proposal solves, that I could see clearly, is boilerplate in error handling. If that's the only reason, then it's not worth it to me. The argument about syntactic boilerplate is actually working against proposal. It's much more complex in that regard - all those magic symbols and brackets that are so easy to miss. Example A has boilerplate but it doesn't cause logic errors. In that context, there's nothing to gain from that proposal, again, making it not very useful.

Let's leave Rust features to Rust.

jredville commented 7 years ago

To clarify, I'm not wild about adding the ! suffix as a shortcut, but I do like the idea of coming up with a simple syntax that simplifies

err = foo()
if err != nil {
  return err
}

Even if that syntax is a keyword instead of a special symbol. It's my biggest complaint about the language (even bigger than Generics personally), and I think the littering of that pattern across the code makes it harder to read and noisy.

I also would love to see something that enables the kind of chaining @tarcieri brings up, as I find it more readable in code. I think the complexity @creker alludes to is balanced by the better signal-to-noise ratio in the code.

kr commented 7 years ago

I don't fully understand how this proposal would achieve its stated goals.

  1. Reduce error handling boilerplate: the proposal has some hypothetical Go code:

    result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }

    I'm not really sure how func(resp) { expr } is supposed to work without more extensive changes to the way function literals work. I think the resulting code would end up looking more like this:

    result := doThing(a).
    Then(func(resp T) result(T, error) { return doAnotherThing(b, resp.foo()) }).
    Then(func(resp T) result(T, error) { return FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }

    In realistic Go code, it is also quite common for the intermediate expressions to be longer than this and to need to be put on their own lines. This happens naturally in real Go code today; under this proposal, it would be:

    result := doThing(a).
    Then(func(resp T) result(T, error) {
        return doAnotherThing(b, resp.foo())
    }).
    Then(func(resp T) result(T, error) {
        return FinishUp(c, resp.bar())
    })
    
    if result.isError() {
    return result.Error()
    }

    Either way, this strikes me as okay, but not great, just like the real Go code above it in the proposal. Its 'Then' combinator is essentially the opposite of 'return'. (If you are familiar with monads, this will not come as a surprise.) It removes the requirement to write an 'if' statement, but introduces the requirement to write a function. Overall, it's not substantially better or worse; it is the same boilerplate logic with a new spelling.

  2. Allows the compiler to reason about results: if this feature is desirable (and I'm not expressing any opinion about that here), I don't see how this proposal makes it substantially more or less feasible. They strike me as orthogonal.

  3. Error handling combinators: this goal is certainly achieved by the proposal, but it is not entirely clear that it would be worth the cost of the necessary changes to achieve it, in the context of the Go language as it stands today. (I think this is the main point of contention in the discussion so far.)

In most well-written Go, this kind of error-handling boilerplate makes up a small fraction of code. It was a single-digit percentage of lines in my brief look at some Go codebases I consider to be well-written. Yes, it is sometimes appropriate, but often it's a sign that some redesign is in order. In particular, simply returning an error without adding any context whatsoever happens more often than it should today. It might be called an "anti-idiom". There's a discussion to be had around what, if anything, Go should or could do to discourage this anti-idiom, either in the language design, or in the libraries, or in the tooling, or purely socially, or in some combination of those. I would be equally interested to have that discussion whether or not this proposal is adopted. In fact, making that anti-idiom easier to express, as I believe is the aim of this proposal, might set up the wrong incentives.

At the moment, this proposal is being treated largely as matter of taste. What would make it more compelling in my opinion would be evidence demonstrating that its adoption would reduce the total amount of bugs. A good first step might be converting a representative chunk of the Go corpus to demonstrate that some sorts of bugs are impossible or unlikely to be expressed in the new style — that x bugs per line in actual Go code in the wild would be fixed by using the new style. (It seems much harder to demonstrate that the new style doesn't offset any improvement by making other sorts of bugs more likely. There we might have to make do with abstract arguments about readability and complexity, like in the bad old days before the Go corpus rose to prominence.)

With supporting evidence like that in hand, one could make a stronger case.

peterbourgon commented 7 years ago

Simply returning an error without adding any context whatsoever happens more often than it should today. It might be called an "anti-idiom".

I'd like to echo this sentiment. This

if err := foo(x); err != nil {
    return err
}

should not be simplified, it should be discouraged, in favor of e.g.

if err := foo(x); err != nil {
    return errors.Wrapf(err, "fooing %s", x)
}
urandom commented 7 years ago

@peterbourgon

my biggest problem with this is not that the error is returned blindly. It's the fact that the action: foo(x); isn't that visible, and imho makes the whole thing quite a bit less readable than alternate 'functional' solutions, where the action itself is a simple return on a new line.

even if the assignment and action is kept separate from the if statement itself, the resulting statement would still put an accent on the result, rather than the action. That is perfectly valid, especially if the result is the important part. But if you have a bunch of statements, where each one gets a (result, error) tuple, checks the error/returns, then proceeds to do another action while obtaining a new tuple, the results themselves are obviously not the main characters in the plot.

iporsut commented 7 years ago

@urandom I think result is pair of (val, error) so I think checks the error/returns are the main characters in the plot too.

novikk commented 7 years ago

What about a reserved word (something like reterr) to avoid all the if err != nil { return err }?

So this

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

Would become:

resp, _ := reterr doThing(a)
resp, _ = reterr doAnotherThing(b, resp.foo())
resp, _ = reterr FinishUp(c, resp.bar())

reterr would basically check the return values of the called function and return if any of them is error and is not nil (and return nil in any non-error return value).

egorse commented 7 years ago

Sounds more and more as #18721

iporsut commented 7 years ago

@tarcieri Just use some of reflect package. I can simulate something like your proposal. But I think it not worth to do it.

https://play.golang.org/p/CC5txvAc0e

func main() {

    result := Do(func() (int, error) {
        return doThing(1000)
    }).Then(func(resp int) (int, error) {
        return doAnotherThing(200000, resp)
    }).Then(func(resp int) (int, error) {
        return finishUp(1000000, resp)
    })

    if result.err != nil {
        log.Fatal(result.err)
    }

    val := result.val.(int)
    fmt.Println(val)
}
tarcieri commented 7 years ago

@iporsut there are two problems with reflection which make it an unsuitable solution to this particular problem, although it may appear to "solve" the problem on the surface:

  1. No type safety: with reflection we cannot determine at compile time if the closure is suitably typed. Instead our program will compile regardless of the types, and we'll encounter a runtime crash if they're mismatched.
  2. Huge performance overhead: the approach you're suggesting is not too far off from the one offered by go-linq. They claim using reflection for this purpose is "5x-10x slower". Now imagine this amount of overhead at every single call site.

To me either of these problems are a huge step backward from what Go has already, and in tandem they're a complete nonstarter.

Kiura commented 7 years ago

I like Go and the way it handles errors. However, maybe it could be simpler. Here are some of my ideas regarding error handling in Go.

The way it is now:


resp, err := doThing(a)
if err != nil {
    return nil, err
}

resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}

resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

A:


resp, _ := doThing(a) 
resp, _ = doAnotherThing(b, resp.foo())
resp, _ = FinishUp(c, resp.bar())
// return if error is omited, otherwise deal with it as usual (if err != nil { return err })
//However, this breaks semantics of Go and may mislead due to the usa of _ (__ or !_ could be used to avoid such misleading)

B:


resp, err := doThing(a)?
resp, err = doAnotherThing(b, resp.foo())?
resp, err = FinishUp(c, resp.bar())?
// ? indicates that it will return in case of error (more explicit)
// or any other indication could be used
// this approach is preferred for its explicitness

C:


resp, err := doThing(a)
return if err

resp, err = doAnotherThing(b, resp.foo())
return if err

resp, err = FinishUp(c, resp.bar())
return if err
// if err return err
// or if err return (similar to javascript return)
// this one is my favorite, almost no changes to the language, very readable and less SLOC

D:


resp, _ := return doThing(a)
resp, _ = return doAnotherThing(b, resp.foo())
resp, _ = return FinishUp(c, resp.bar())
// or 
resp = throw FinishUp(c, resp.bar())
// this one is also very readable (although maybe a litle less than option **C**) and even less SLOC than **C**
// at this point I'm not sure whether C or D is my favorite )) 

//This applies to all approaches above
// if the function that contains any of these options has no value to return, exit the function. E.g.:
func test() {
    resp, _ := return doThing(a) // or any of other approaches
    // exit function
}

func test() ([]byte, error) {
    resp, _ := return doThing(a) // or any of other approaches
    // return whatever is returned by doThing(a) (this function of course must return ([]byte, error))
}

Excuse my English and I am not sure whether such changes are possible and whether they will result in performance overhead.

If you like any of these approaches, please like them following next rules:

A = 👍 B = 😄 C = ❤️ D = 🎉

And 👎 if you dislike the whole idea ))

This way we can have some statistics and avoid unnecessary comments like "+1"

Kiura commented 7 years ago

Eloborating on my "proposals"...

// no need to explicitely define error in return statement, much like throw, try {} catch in java
func test() int {
     resp := throw doThing() // "returns" error if doThing returns (throws) an error
     return resp // yep, resp is int
}

func main() {
     resp, err := test() // the last variable is always error type
     if err != nil {
          os.Exit(0)
     }
}

Again, not sure if something like that is possible at all ))

alercah commented 7 years ago

Here's another crazy option, make the word error a little more magic. It becomes usable on the left-hand side of an assignment (or short declaration) and works sort of like a magic function:

res, error() := doThing()
// Shorthand for
res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

Specifically, the behaviour of error() is as follows:

  1. It is treated like it has type error for the purposes of assignment.
  2. If nil is assigned to it, nothing happens.
  3. If a non-nil value is assigned to it, the enclosing function immediately returns. All return values are set to 0 except for the the last, which must be of type error and which is assigned the value assigned to error().

If you want to apply some mutation to the error, then you can do:

res, error(func (e error) error { return fmt.Errorf("foo: %s", error)})
  := doThing()

In which case the closure is applied to the value assigned before the function returns.

This is a bit ugly, in large part due to the syntactic bloat of having to deal with closures. The standard library could fix this well, with e.g. error(errors.Wrapper("foo")) which will generate the correct wrapper closure for you.

As an alternative, if the nullary error() syntax is too likely to be missed, I'd suggest error(return) as an alternative; use of the keyword reduces risk of misinterpretation. It doesn't extend well to the closure case, however.

bhinners commented 7 years ago

Everyone who's written Go has encountered the unfortunate proliferation of error handling boilerplate that distracts from the core purpose of their code. That's why Rob Pike addressed the subject in 2015. As Martin Kühl points out, Rob's proposal for simplifying error handling:

leaves us having to implement artisanal one-off monads for every interface we want to handle errors for, which I think is still as verbose and repetitive

Which is why there's so much engagement on this topic still today.

Ideally we can find a solution which:

  1. Reduces repetitive error handling boilerplate and maximizes focus on the primary intent of the code path.
  2. Encourages proper error handling, including wrapping of errors when propagating them onward.
  3. Adheres to the Go design principles of clarity and simplicity.
  4. Is applicable in the broadest possible range of error handling situations.

I propose the introduction of a new keyword catch: which works as follows:

Instead of the current form:

res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

we would write:

res, err := doThing() catch: 0, ..., 0, err

which would behave in exactly the same manner as the current form code above. More specifically, the function and assignments to the left of the catch: are executed first. Then, if and only if exactly one of the return arguments is of type error AND that value is non-nil, the catch: acts as a return statement with the values to the right. If there are zero or more than one error type returned from doThing(), it's a syntax error to use catch:. If the error value returned from doThing() is nil, then everything from catch: to the end of the statement is ignored and not evaluated.

To give a more complex example from Nemanja Mijailovic's recent blog post entitled, Error handling patterns in Go:

func parse(r io.Reader) (*point, error) {
  var p point

  if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
    return nil, err
  }

  return &p, nil
}

This becomes instead:

func parse(input io.Reader) (*point, error) {
  var p point

  err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
  err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
  err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
  err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
  err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

  return &p, nil
}

Advantages:

  1. Close to minimum additional boilerplate for error handling.
  2. Improves focus on the primary intent of the code with minimal error handling baggage on the left side of the statement and error handling localized on the right side.
  3. Works in many different situations giving the programmer flexibility in the case of multiple return values (e.g. if you want to return an indicator of the count of items that succeeded in addition to the error).
  4. Syntax is simple and would be easily understood and adopted by Go users, both new and old.
  5. Partially succeeds in encouraging proper error handling by making error code more succinct. May make error code slightly less likely to be copy-pasted and thereby reduce introduction of common copy-paste errors.

Disadvantages:

  1. This approach doesn't fully succeed in encouraging proper error handling because it does nothing to promote wrapping errors before propagating them. In my ideal world, this new syntax would have required that the error returned by catch: either be a new error or a wrapped error, but not identical to the error returned by the function to the left of the catch:. Go has been described as "opinionated" and such strictness on error handling for the sake of clarity and reliability would have fit with that. I lacked the creativity to incorporate that goal, though.
  2. Some may argue that this is all syntactic sugar and not needed in the language. A counter argument might be that the current error handling in Go is syntactic trans fat, and this proposal just eliminates it. To be widely adopted, a programming language should be pleasurable to use. Largely Go succeeds at that, but the error handling boilerplate is a particularly profuse exception.
  3. Are we "catching" the error from the function we call, or are we "throwing" an error to whoever called us? Is it appropriate to have a catch: without an explicit throw? The reserved word doesn't necessarily have to be catch:. Others may have better ideas. It could even be an operator instead of a reserved word.
cznic commented 7 years ago

Everyone who's written Go has encountered the unfortunate proliferation of error handling boilerplate that distracts from the core purpose of their code.

That is not true. I do program in Go quite a lot and I do not have any problem with any error handling boilerplate. Writing error handling code consumes such a microscopic fraction of time developing a project that I hardly notice it and it IMHO it does not justify any change to the language.

bhinners commented 7 years ago

Everyone who's written Go has encountered the unfortunate proliferation of error handling boilerplate that distracts from the core purpose of their code.

That is not true. I do program in Go quite a lot and I do not have any problem with any error handling boilerplate. Writing error handling code consumes such a microscopic fraction of time developing a project that I hardly notice it and it IMHO it does not justify any change to the language.

I didn't say anything about how much time writing error handling code takes. I only said that it distracts from the core purpose of the code. Maybe I should have said "Everyone who's read Go has encountered the unfortunate proliferation of error handling...".

So, @cznic, I guess the question for you is whether you've read Go code that you felt had an excessive amount of error handling boilerplate or which distracted from the code you were trying to understand?

Kiura commented 7 years ago

No one likes my proposals 😅 Anyways, we should have some syntax, and vote for best one (some poll system) and include link here or in readme

cznic commented 7 years ago

Maybe I should have said "Everyone who's read Go has encountered the unfortunate proliferation of error handling...".

That's not true. I prefer the explicitness and proper locality of the current state of art of error handling. The proposal, as any other I have ever seen, makes the code IMHO less readable and worse to maintain.

So, @cznic, I guess the question for you is whether you've read Go code that you felt had an excessive amount of error handling boilerplate or which distracted from the code you were trying to understand?

No. Go is in my experience an exceptionally well readable programming language. Half of that credit goes to gofmt, of course.

alercah commented 7 years ago

My own experience is that it really starts to drag when you have a bunch of dependent statements, each of which can throw an error, the error handling adds up and gets old fast. What could be 5 lines of code becomes 20.

urandom commented 7 years ago

@cznic In my experience, having so much error handling boilerplate makes the code much less readable. Because the error handling itself is mostly identical (sans any error wrapping that might occur), it produces a sort-of fence effect, where if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling. Thus the biggest problem, the actual code, the most important part of the program, is hidden behind this optical illusion, making it that much difficult to actually see what a piece of code is about.

Error handling shouldn't the main part of any code. Unfortunately, quite often it ends up being exactly that. There's a reason statement composition in other languages is so popular.

davecheney commented 7 years ago

Because the error handling itself is mostly identical (sans any error wrapping that might occur), it produces a sort-of fence effect, where if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling.

This is a highly subjective position. It's like arguing that if statements make the code unreadable, or that K&R style braces make things unreadable.

From my point of view the explicitness of go's error handling quickly fades into the background of familiarity until you notice the pattern broken; something the human eye is very good at doing; missing error handling, error variables assign to _, etc.

It is a burden to type, make no mistake. But Go does not optimise for the code author, it explicitly optimises for the reader.

On Tue, May 16, 2017 at 5:45 PM, Viktor Kojouharov <notifications@github.com

wrote:

@cznic https://github.com/cznic In my experience, having so much error handling boilerplate makes the code much less readable. Because the error handling itself is mostly identical (sans any error wrapping that might occur), it produces a sort-of fence effect, where if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling. Thus the biggest problem, the actual code, the most important part of the program, is hidden behind this optical illusion, making it that much difficult to actually see what a piece of code is about.

Error handling shouldn't the main part of any code. Unfortunately, quite often it ends up being exactly that.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/19991#issuecomment-301702623, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAcA4ydpBFiapYBOBUyUjg6du5Dnjs5ks5r6VQjgaJpZM4M-dud .

bhinners commented 7 years ago

if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling.

This is a highly subjective position.

Highly subjective yet widely shared.

As Rob himself said,

A common point of discussion among Go programmers, especially those new to the language, is how to handle errors. The conversation often turns into a lament at the number of times the sequence

if err != nil {
    return err
}

shows up.

In fairness, Rob went on to say this perception about Go error handling is "unfortunate, misleading, and easily corrected." However, he spends most of that article explaining his recommended method for correcting the perception. Unfortunately, Rob's prescription is problematic in itself as explained so well by Martin Kühl. In addition to Martin's critique, Rob's suggestion also reduces the locality which @cznic says he values in Go error handling.

Maybe the question is if we had the ability to replace

res, err := doThing()
if err != nil {
  return nil, err
}

with something similar to:

res, err := doThing() catch: nil, err

Would you use it, or would you stick with the four line version? Regardless of your personal preference, do you think an alternative like this would be widely adopted by the Go community and become idiomatic? Given the subjectivity of any argument that the shorter version adversely affects readability, my experience with programmers says they would strongly gravitate toward the single line version.

davecheney commented 7 years ago

Real talk : go 1 is fixed, and will not change, especially in this fundamental way.

It's pointless to propose some kind of option type until Go 2 implements some for of templates type. At that point, everything changes.

On 16 May 2017, at 23:46, Billy Hinners notifications@github.com wrote:

if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling.

This is a highly subjective position.

Highly subjective yet widely shared.

As Rob himself said,

A common point of discussion among Go programmers, especially those new to the language, is how to handle errors. The conversation often turns into a lament at the number of times the sequence

if err != nil { return err } shows up.

In fairness, Rob went on to say this perception about Go error handling is "unfortunate, misleading, and easily corrected." However, he spends most of that article explaining his recommended method for correcting the perception. Unfortunately, Rob's prescription is problematic in itself as explained so well by Martin Kühl. In addition to Martin's critique, Rob's suggestion also reduces the locality which @cznic says he values in Go error handling.

Maybe the question is if we had the ability to replace

res, err := doThing() if err != nil { return nil, err } with something similar to:

res, err := doThing() catch: nil, err

Would you use it, or would you stick with the four line version? Regardless of your personal preference, do you think an alternative like this would be widely adopted by the Go community and become idiomatic? Given the subjectivity of any argument that the shorter version adversely affects readability, my experience with programmers says they would strongly gravitate toward the single line version.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

bhinners commented 7 years ago

It's pointless to propose some kind of option type until Go 2 implements some for of templates type. At that point, everything changes.

I assumed we were talking about Go 2 as implied by the title of this thread and in the full belief that "Go 2" is not a euphemism for "never". In fact, given that Go 1 is fixed, we should probably be devoting a much larger portion of our Go discussions to Go 2.

davecheney commented 7 years ago

With that said I think everyone who complains about the verbosity of Go's error handling is missing the fundamental point that the purpose of error handling in Go is not to make the not error case as brief and unobtrusive as possible. Rather the goal of Go's error handling strategy is to force the writer of the code to consider, at all times, what happens when the function fails, and, most importantly, how to clean up, undo, and recover before returning to the caller.

All the stragies for hiding error handling boiler plate seem to me to be ignoring this.

On Tue, 16 May 2017, 23:51 Dave Cheney dave@cheney.net wrote:

Real talk : go 1 is fixed, and will not change, especially in this fundamental way.

It's pointless to propose some kind of option type until Go 2 implements some for of templates type. At that point, everything changes.

On 16 May 2017, at 23:46, Billy Hinners notifications@github.com wrote:

if you quickly scan through a piece of code, you mostly end up seeing a mass of error handling.

This is a highly subjective position.

Highly subjective yet widely shared.

As Rob himself said,

A common point of discussion among Go programmers, especially those new to the language, is how to handle errors. The conversation often turns into a lament at the number of times the sequence

if err != nil { return err }

shows up.

In fairness, Rob went on to say this perception about Go error handling is "unfortunate, misleading, and easily corrected." However, he spends most of that article https://blog.golang.org/errors-are-values explaining his recommended method for correcting the perception. Unfortunately, Rob's prescription is problematic in itself as explained https://www.innoq.com/en/blog/golang-errors-monads/ so well by Martin Kühl. In addition to Martin's critique, Rob's suggestion also reduces the locality which @cznic https://github.com/cznic says he values in Go error handling.

Maybe the question is if we had the ability to replace

res, err := doThing() if err != nil { return nil, err }

with something similar to:

res, err := doThing() catch: nil, err

Would you use it, or would you stick with the four line version? Regardless of your personal preference, do you think an alternative like this would be widely adopted by the Go community and become idiomatic? Given the subjectivity of any argument that the shorter version adversely affects readability, my experience with programmers says they would strongly gravitate toward the single line version.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/19991#issuecomment-301787215, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAcAwATgoJwL5WV-0nffLjLB9L86GYOks5r6ai3gaJpZM4M-dud .

creker commented 7 years ago

Rather the goal of Go's error handling strategy is to force the writer of the code to consider, at all times, what happens when the function fails, and, most importantly, how to clean up, undo, and recover before returning to the caller.

Well, then Go didn't achieve that goal. By default, Go allows you to ignore returned errors and in many cases you wouldn't even know about that until something somewhere wouldn't work like it should. On the contrary, much hated in Go community exceptions (that's just an example to prove the point) force you to consider them because otherwise application will crash. That often leads us to problem with catching everything and ignoring but that's programmer's fault.

Basically, error handling in Go is opt-in. It's more about spoken convention that every error should be handled. The goal would be achieved if it would actually force you to handle errors. For example, with compile-time errors or warnings.

With that in mind, hiding boiler plate would not hurt anybody. Spoken convention would still hold and programmers would still opt-in to error handling as it is right now.

bhinners commented 7 years ago

the goal of Go's error handling strategy is to force the writer of the code to consider, at all times, what happens when the function fails, and, most importantly, how to clean up, undo, and recover before returning to the caller.

That's an inarguably noble goal. It's a goal, though, that must be balanced against the readability of the primary flow and intent of the code.

davecheney commented 7 years ago

As a Go programmer, I can say to you that I do not find the verbosity of Go's error handling to hurt it's readability. I don't see there is anything to trade away, because I feel no discomfort reading code written by other Go programmers.

On Wed, May 17, 2017 at 12:10 AM, Billy Hinners notifications@github.com wrote:

the goal of Go's error handling strategy is to force the writer of the code to consider, at all times, what happens when the function fails, and, most importantly, how to clean up, undo, and recover before returning to the caller.

That's an inarguably noble goal. It's a goal, though, that must be balanced against the readability of the primary flow and intent of the code.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/19991#issuecomment-301794653, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAcAzfcu5hq86xxVj85qfOquVawHh44ks5r6a5zgaJpZM4M-dud .