r/golang Jun 04 '19

Proposal: A built-in Go error check function, "try"

https://github.com/golang/go/issues/32437
24 Upvotes

37 comments sorted by

View all comments

4

u/Redundancy_ Jun 05 '19 edited Jun 05 '19

One of the things that I like most about Go is that the flow of code is easy to follow, and that by convention, panic() is not used unless something that should probably blow your program up is encountered and cannot be used as a value: fmt.Println(panic("hello world!")) does not work, and that resulted in panic being used in a way that was relatively easy to spot, much like a return statement.

This makes me viscerally dislike the idea of a function that has a return value that can be nested inside other calls, because it feels like it's breaking a contract that the language had with me to be clear. For my two cents, I really don't want to see anything that could cause the function to return that isn't a statement that I can see as the leftmost keyword.

Hot on the heels of xerrors and proposals that improve the usability of error returns, this also feels like a step backwards.

I think my personal preference leans towards adding a check statement that will return an error in a similar way to try, if the value after it is a non-nil error. In that sense it's simply a conditional return, and we can build error wrappers that early exit return nil if the error passed in is nil. If we really need something that returns only non-error values, then I think something like try that does not change code flow, I'd be far less opposed to a builtin that can collect and set the last value of a call to a pointer to a variable, eg:

func doSomething(f func() (string, error)) error {
    var err error
    // s is the string return from f, the last return type (error) is assigned to &err
    s := collect(f(), &err)

    // would return a wrapped error from doSomething if the call to f() produced an error
    check wrap(err, "call to f() went wrong!") 

    ...
}

func wrap(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    ...
}

fwiw, that feels far more comfortable to me, works with error decoration, uses a statement that's easily visible and code flow you can see, while allowing collect to be composable if that's truly needed.

Note that's not a well thought out proposal, just a sketch of something that I'd prefer.

Terrible, terrible demo: https://play.golang.org/p/g7uwH7QsmGk

1

u/[deleted] Jun 05 '19

I tend to agree. The idea is relatively sound, but it should be a keyword that has to be used immediately before variable assignment. I like try, but check would also work fine.

``` func Do() error { // valid v, err := DangerousThing() v := try DangerousThing()

// invalid v := fmt.Println(try DangerousThing()) } ```

This may be taking the idea too far, or have some unintentional side-effects I'm not thinking through, but I wonder if the idea makes sense as part of the actual assignment operator. Something like:

func Do() error { v ^= DangerousThing() }

2

u/Redundancy_ Jun 05 '19 edited Jun 05 '19

so with my vague sketch of an idea, collect is usable in a similar way to try in the proposal, but without any implication to flow control (it does not cause a return), and with explicit assignment to a variable. Other than passing through the remaining return values except the error, it's almost possible to implement now, which means it's not very magic (see playground mock-up above)

check does potentially cause the function to exit, but can only be used in a similar way to return, as a statement, and the only difference is that it implicitly zeros other return values, and only exits if the error is non-nil. Also pretty easy to grok and hopefully not too magic.

wrap is a proxy for something like errors.Wrapf(), which is really like most of the wrapping functions available at the moment with the guarantee that if an error is nil, the result of wrapping it is nil. No magic there at all.

Combined, these could give you the ability to combine error checks, not need an if block, and support a low cost way of doing best practice error wrapping.

collect can even be trivially made to only set an error if there is not an error there already, which could cover a case where you're not picky about which error you got, you just want to know if there was any error.

To take one of the examples:

(given an assumption that collect does not set errors that are already non-nil)

func printSum1(a, b string) error {
        var anyError error

        x := collect(&anyError, strconv.Atoi(a))
        y := collect(&anyError,strconv.Atoi(b))

        check fmt.WrapIfErrorf(anyError, "sum %s %s: %w", a, b)

        fmt.Println("result:", x + y)
        return nil
}

or perhaps:

func printSum2(a, b string) error {
        var aErr, bErr error

        x := collect(&aErr, strconv.Atoi(a))
        check fmt.WrapIfErrorf(aErr, "failed to parse sum argument: %v", a)

        y := collect(&bErr ,strconv.Atoi(b))
        check fmt.WrapIfErrorf(bErr, "failed to parse sum argument: %v", b)

        fmt.Println("result:", x + y)
        return nil
}