Go: proposal: Go 2: revisit explicit annotations in :=

Created on 4 Aug 2017  Â·  24Comments  Â·  Source: golang/go

Before Go 1 := would only work if every name on the lhs had not been declared in the current scope. This made it hard to write common code like

x, err := f()
//...
y, err := g()

Either y would require an explicit declaration with var or there would need to be an ugly workaround such as

y, err2 := g()
err = err2

Among other peccadilloes this meant copying a bit of valid code from one place to another could make it invalid and it would need to be rewritten.

This made := much less useful than it could be so the current behavior of implicitly reusing existing names defined in the current scope was introduced and erroring out if none of the names were new.

This made := much more useful, but has made it too easy to shadow variables in outer scopes.

When it was introduced, there was much discussion in the mailing list thread announcing the change (apologies but I cannot find a link) for a way to explicitly annotate the names to reuse such as

y, ^err := g() // mnemonic for "reusing variable defined above"

This was dismissed—if I recall correctly (again apologizes for not being able to track down the original thread)—because it would mean that copying a bit of valid code from one place to another could make it invalid, as annotations would need to be added or removed depending on the new context.

However, this is subtly true with the current behavior := depending on the names involved and what is or is not in the current scope, leading to bugs caused by accidental shadowing that are hard to track down due to the implicit nature of what is shadowed when. This is particularly vexing with named returns.

With explicit annotation it would still be possible to accidentally shadow a variable, but it would be easier to see what was happening at a glance and possibly easier to detect mechanically.

I think the idea of explicit annotation should be revisited for Go 2. I propose the following semantics

  1. x, y, z := f() follows the pre-Go 1 semantics—all names must be unbound in the current scope
  2. x, ^y, ^z := f() reuses y and z as defined in the current scope or an outer but non-global scope†
  3. annotating a name that does not exist is an error
  4. at least one name on the lhs must not be annotated

† making it incompatible with reusing variables in package global scope is a bit uneven but seems a wise safety precaution

This should be entirely go fix-able as the correct annotations to mirror the Go 1 semantics could be applied mechanically.

FrozenDueToAge Go2 LanguageChange Proposal

Most helpful comment

I would like it if shadowing an outer scope in the short declaration were banned.

So, for example, this would be legal:

func Example() (int, error) {
    x, err := thingOne()
    if err != nil {
        return 0, err
    }
    y, err := thingTwo()
    if err != nil {
        return 0, err
    }
    return x+y, nil
}

Because while err is reused, it does not shadow an outer err.

But this would be illegal:

func Example() (int, error) {
    x, err := thingOne()
    if err != nil {
        return 0, err
    }
    if x > 1 {
        y, err := thingTwo()
        if err != nil {
            return 0, err
        }
        x += y
    }
    return x, nil
}

Because the inner err shadows an outer one.

I think something like a shadow ban is necessary to prevent the footgun of accidental shadowing.

All 24 comments

Dup of #377?

That thread covers a lot of potential changes to :=. It looks like the explicit annotation was brought up after a dozen or so posts and eventually dominates the conversation (I only skimmed). I don't mind this being closed as a dup but #377 is closed due to age.

Before Go 1 := would only work if every name on the lhs had not been
declared in the current scope. This made it hard to write common code like

x, err := f()
//...
y, err := g()

This is not correct,

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

On Fri, Aug 4, 2017 at 10:04 AM, Brad Fitzpatrick notifications@github.com
wrote:

Dup of #377 https://github.com/golang/go/issues/377?

—
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/21303#issuecomment-320121591, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAAcA9ajSza1u0_AHhT0L619SCUtrY4qks5sUmAjgaJpZM4OtIK3
.

@davecheney that's Go 1 code.

^ is the bitwise xor operator.

I'm not really sure this is that useful to be honest.
If I think something's going to be shadowed, I usually explicitly declare the variables.

Another alternative would be to annotate the new names instead of the old: that would make a ShortVarDecl equivalent to an Assignment.

For example, we could shift the colons from the = symbol to the new names:

x:, err: = f()
// …
y:, err = g()

I don't really care about the syntax. There are other suggestions in #377 and the mailing list thread linked in there. I'm sure the Go team will pick something reasonable and pleasant in the face of all other constraints, if they decide to implement.

I'll use ^ in any examples that I write for consistency with the first post. I know it's an operator already but I think the polysemy should be fine in this case since a ^ would already be illegal in any place where this would come up, but, if it's not, that's fine.

I just want to be able to explicitly and concisely reuse variables from outer scopes without having to entirely and awkwardly rewrite the line with multiple var declarations. I want it to be easier to spot and avoid shadowing.

Annotating new names rather than old names would make it easier to avoid accidental shadowing in the first place.

I was thinking that another option would be to keep the default rules of := as it is now, but allow annotations to mean "use the one in an outer scope". That way Go 1 and Go 2 code would be source compatible (wrt this feature, at least) without needing a go fix. I'm not sure how far the plan is to go with that since that would mean no new keywords. It would mean you'd only be able to annotate old names, though.

I'd prefer "you always need to annotate" as it would mean not seeing an annotation had as much meaning as seeing an annotation, even though that would mean having to annotate more often.

I don't really care about the syntax.

Then why not just use

x, err := f()
y, e2 := g(); err=e2

Before Go 1 := would only work if every name on the lhs had not been declared in the current scope.

I don't believe this is true, although I remember the discussion on changing the semantics of :=.
See my earlier comment: https://github.com/golang/go/issues/21161#issuecomment-320635780

From that previous discussion, ISTR favouring a syntax like this:

:a, :err = foo()     // equivalent to a, err := foo()
:b, err = foo()      // b is newly declared; err is not.

This doesn't avoid accidental shadowing though - I'm pretty sure you
don't want to ban all shadowed variable names.

@as I don't care about the specific syntax used as long as there is some syntax with the desired semantics

@rogpeppe I could easily be misremembering or confusing it with a comment from the Go team that "it worked like that but then we changed it". Maybe even another language entirely. I tried to find the thread to check before posting but could not so I crossed my fingers and posted. Oh well.

At any rate, no I don't want to ban shadowing.

Shadowing is good. But it can also be awkward and easy to shadow too much.

The proposal is for some way to keep the nice bits of := but add a way to explicitly allow reuse of a variable defined in an outer scope. The current behavior can cause subtle problems especially with named returns and common names like err so use of := requires a dose of paranoia.

If that's the :a, err = foo() syntax, great.

I would like it if shadowing an outer scope in the short declaration were banned.

So, for example, this would be legal:

func Example() (int, error) {
    x, err := thingOne()
    if err != nil {
        return 0, err
    }
    y, err := thingTwo()
    if err != nil {
        return 0, err
    }
    return x+y, nil
}

Because while err is reused, it does not shadow an outer err.

But this would be illegal:

func Example() (int, error) {
    x, err := thingOne()
    if err != nil {
        return 0, err
    }
    if x > 1 {
        y, err := thingTwo()
        if err != nil {
            return 0, err
        }
        x += y
    }
    return x, nil
}

Because the inner err shadows an outer one.

I think something like a shadow ban is necessary to prevent the footgun of accidental shadowing.

@carlmjohnson
What about this example?

func Example() (int, error) {
    x, err := thingOne()
    if err != nil {
        return 0, err
    }
    f := func() int {
        y, err := thingTwo()
        if err != nil {
            log.Println(err)
        }
        return y
    }
    return f(), nil
}

I think that having to call the error err2 is a small price to pay for making closures close over what you expect in other cases.

How about ban short declarations except the ones in the first parts of control flow blocks?

Short declarations bring too many confusions than their convenience. And they are much less readable than the long declarations.

How about ban short declarations except the ones in the first parts of control flow blocks?

I'll go further: How about we have just one assignment syntax? Allow var syntax in control flow initializers, and use it everywhere. Or allow := syntax to specify types and use that everywhere instead (assigning the zero value where applicable).

I don't care too much what the syntax _is_, but it bothers me that there are multiple syntaxes and I have to remember which one to use when.

I confess that something about having two different declarations makes me itch just a little bit. If the short declaration is removed, the previous example becomes...

func Example() (int, error) {
    var x int
    var err error
    x, err = thingOne()
    if err != nil {
        return 0, err
    }
    if x > 1 {
        var y int
        y, err = thingTwo()
        if err != nil {
            return 0, err
        }
        x += y
    }
    return x, nil
}

Or, to shadow by way of scoping,

func Example() (int, error) {
    var x int
    var err error
    x, err = thingOne()
    if err != nil {
        return 0, err
    }
    if x > 1 {
        var y int
        var err error
        y, err = thingTwo()
        if err != nil {
            return 0, err
        }
        x += y
    }
    return x, nil
}

However, effective redeclaration via shadowing would be disallowed:

func Example() (int, error) {
    var x int
    var err error
    x, err = thingOne()
    if err != nil {
        return 0, err
    }

    var y int
    var err error // ERROR
    y, err = thingTwo()
    if err != nil {
       return 0, err
    }
    x += y

    return x, nil
}

I don't care for the added verbosity, and I'm rather fond of the := operator. But like this it _is_ extremely explicit, which improves readability, both of which are nice.

Going off on a slight tangent, the verbosity above could be improved if we could use var to declare multiple variables, using the same syntax as function parameters:

var x, y int, err error

I really can't imagine that Go 2 would remove either := or var. Yes, they do similar things, but they are not the same. We can usefully discuss tweaks to them, such as this proposal, but I don't see any point to discussing actually removing one or the other.

Commenting to add a cross-reference to #21114.

Closing as dup of #377.

@ianlancetaylor #377 is "locked and limited to collaborators."

@jimmyfrasche Are you disagreeing that this should be closed as a dup?

Still, I'll unlock #377 for now.

No, it is a dup. I had assumed that by posting a summary in #377 that you were attempting to consolidate the discussion there which wouldn't work very well if no one could discuss. I figured you either forgot to unlock it or didn't notice it was locked and was trying to be helpful.

@jimmyfrasche Thanks. I sincerely hope that #377 doesn't turn into a long-running discursive discussion. I don't think that is helpful.

Was this page helpful?
0 / 5 - 0 ratings