go: proposal: Go 2: support "assign if nil" statement to tackle error handling boilerplate

Hello,

it seems to me that when people complain about Go’s error handling, they mostly complain about the situations when it gets repetitive (otherwise I assume it’s not a big problem), like this:

a, b, err := f1()
if err != nil {
    return nil, errors.Wrap(err, "failed")
}
c, err := f2(a)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}
d, err := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

Now, I usually don’t encounter code like this, but I agree that this looks awful and is hard to prevent sometimes.

So I came up with a simple solution. Keep in mind, the solution is just an idea which might be iterated upon, or completely thrown away.

Assign if nil statement

Here’s my solution.

In an assignment statement (= or :=), when any LHS operand is surrounded by parenthesis, the assignment will evaluate and assign if and only if all parenthesis-surrounded LHS operands are zero value (nil or an appropriate equivalent)

In case of := assignment, variables undeclared before are treated as zero valued.

What I mean is basically that this

a, b, (err) := f1()

gets translated to this

// assume a, b and err are declared
if err == nil {
    a, b, err = f1()
}

In general, something like this

a, b, (c), (d) := f()

gets translated to

if c == nil && d == nil {
    a, b, c, d = f()
}

How does this help?

With “assign if nil” statement, we can rewrite the original error handling chain like this:

a, b, (err) := f1()
c, (err) := f2(a)
d, (err) := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

In the first line, err is undeclared and thus is handled as if it was nil and f1 executes. Next line only executes f2 and assigns the values if err is still nil, i.e. when the previous call finished without errors. The same holds for the third line.

In the last line, we nicely wrap the error with additional context information and return it.

Other uses

There are more uses of the “assign if nil” statement. For example, lazily initializing a map:

func (s *Struct) Method() {
    (s.m) = make(map[string]string) // this only gets executed if s.m is nil
}

Summary

I propose introducing an “assign if nil” statement. Proper formal specification is not done here, but is easy to imagine.

I think this construct solves most of the hassle with error handling in Go. An if err != nil once a while is perfectly fine, the only problem is when there is too many of them. I believe that’s almost always possible to avoid using this construct.

Looking forward to your feedback!

Michal Štrba

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 16
  • Comments: 16 (5 by maintainers)

Most upvoted comments

As I mentioned over on #21161, I personally would prefer to see any change to error handling make it just as easy to wrap an individual error with more information as it does to simply move the error along.

@faiface I like the idea, but the syntax has to be worked on. What I dislike is the fact that condition and result is not clearly understandable.

Since go does not have a ternary operator, what about this:

a, b :=  err ? f1()

which could obviously be extended to

a, b :=  err ? f1() : fallbackA, fallbackB

The docs would be as clear as:

“The ternary operator in go differs from that in C in that the condition is a single variable which is implicitly checked for being a zero value”.

@mrkaspa I like the question mark as a replacement for the parenthesis. So I would write this:

a, b, err? := f1()
c, err? := f2(a)
d, err? := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

That’s pretty nice, probably even nicer than my original proposal.

However, I don’t think that automatically returning on ? is a good idea, like Rust does it.

What about if when I do this:

a, b, err? := f1()

expands to this:

if err != nil {
    return nil, errors.Wrap(err, "failed")
}

and I can force it to panic with this:

a, b, err! := f1()

expands to this:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

This will keep backward compatibility, and will mend all the pain points of error handling in go