Go: proposal: spec: various changes to :=

Created on 3 Dec 2009  路  79Comments  路  Source: golang/go

This code is subtly wrong:

func f() (err os.Error) {
  v, err := g();
  if err != nil {
    return;
  }
  if v {
    v, err := h();
    if err != nil {
      return;
    }
  }
}

The := in the if statement causes a new err variable that shadows the 
return parameter.

Maybe doing this should be an error. Maybe return parameters should be 
special so that := doesn't ever shadow them. (I like the latter.)
Go2 LanguageChange NeedsInvestigation Proposal

Most helpful comment

Comment 16:

Another alternative based on the conversation in the mailing list would be to use a
per-variable declaration syntax.
For instance, this:
   a, b := f()
Would be fully equivalent to:
   :a, :b = f()
and a construction in an internal block such as:
   err = f()
might be extended to the following, which is completely clear and unambiguous:
   :a, err = f()
When one doesn't want to redefine err.
One of the things which feels interesting about this proposal is that
it would enable forbidding entirely partial declarations via := if
that's decided to be a good option, without compromising on other
aspects of the language.

All 79 comments

Comment 1:

I noticed this situation a while ago. I argued that it conforms to the scope rules,
which are usual and customary.
The first err is declared under rule 4. The second err is declared under rule 5. The
second declaration is the inner declaration, so the inner redeclaration rule applies,
thereby hiding, within its own scope, the first err.
This is the usual and customary behaviour for many languages. Some languages have a
construct which allows a reference in the inner scope to the variable in the outer scope.
The Go Programming Language Specification
Declarations and scope
The scope of a declared identifier is the extent of source text in which the
identifier denotes the specified constant, type, variable, function, or package.
Go is lexically scoped using blocks:
   1. The scope of a predeclared identifier is the universe block.
   2. The scope of an identifier denoting a constant, type, variable, or function
declared at top level (outside any function) is the package block.
   3. The scope of an imported package identifier is the file block of the file
containing the import declaration.
   4. The scope of an identifier denoting a function parameter or result variable is
the function body.
   5. The scope of a constant or variable identifier declared inside a function
begins at the end of the ConstSpec or VarSpec and ends at the end of the innermost
containing block.
   6. The scope of a type identifier declared inside a function begins at the
identifier in the TypeSpec and ends at the end of the innermost containing block.
An identifier declared in a block may be redeclared in an inner block. While the
identifier of the inner declaration is in scope, it denotes the entity declared by
the inner declaration.

Comment 3 by pshah.foss:

Is there anyway to access the variable in outer scope ?

Comment 4:

_Issue #514 has been merged into this issue._

Comment 5:

This issue now tracks various proposals that have been made, among them:
  * disallow shadowing outer variables
  * allow arbitary expressions on the lhs
  * don't require something new on the lhs

Comment 6:

_Issue #505 has been merged into this issue._

Comment 7:

_Issue #469 has been merged into this issue._

Comment 8 by jesse.dailey:

The go spec for short variable declaration specifically addresses redeclaration, and  
explicitly states that this should not happen.
From the go spec:
"a short variable declaration may redeclare variables provided they were originally 
declared in the same block with the same type"
Right now, you can shadow global variables, and redeclare their type.
"Redeclaration does not introduce a new variable; it just assigns a new value to the 
original."
var someGlobal = "foo";
func someFunc() (int, os.Error) {
  return 1, nil
}
func TestThree(t *testing.T) {
  if someGlobal, err := someFunc(); err == nil {
    // rather than throwing an error, someGlobal will now silently be an int == 1
  }
  // now it will be a string == "foo" again
}

Comment 9:

@jesse.dailey: The implementation is in line with the spec.  The proposal is a change 
to the spec.
x := 1
{ 
    x := 2
}
The two x are in different blocks so the sentence you quoted does not apply.

Comment 10:

another possibility that i think would be useful:
allow non-variables on the l.h.s. of a := as long
as there's one new variable there.
e.g.
   x := new(SomeStruct)
   x.Field, err := os.Open(...)
i actually think this is less controversial than the original
rule allowing non-new variables - at least it's obvious
at a glance which variables have been declared.

Comment 11 by [email protected]:

I think the original poster was making a case for special treatment of return
parameters.  In principle I agree with his argument that it would reduce the
potential for a certain class of subtle errors.  The question is whether this
potential benefit is worth introducing a 'special case' into the spec and eventually
into all go compiler implementations.  Since much is being made by go promoters about
it being a 'safe' language I'm leaning towards agreement with OP, ie. no shadows of
return parameters.  I realize this isn't a democracy, it's just my opinion FWIW  :-)

Comment 12:

_Issue #739 has been merged into this issue._

Comment 13 by snake.scaly:

I think the OP highlights a more general problem: redeclaring variables from outer
scopes can create subtle, hard to track down errors.
Possible solution: make it an error to redeclare a variable declared in the same
function.
Rationale for this language change:
* A whole class of hard to fix errors is eliminated
* Probably it won't hurt most of existing, correct Go code
* Probably it will highlight bugs or hard-to-maintain spots in the existing code
* Redeclaration of global names is still allowed so that a new version of `import .
"Foo"` package won't hijack your code
* Does not complicate specification
* Does not seem to complicate implementation, at least not much

Comment 14:

One thing that could presumably done right now without changing the language is to
provide a warning when shadowing occurs. (Of course then it would be nice to have a
warning level option to give to the compiler so that the warning could be switched off
by people who don't like it.)

Comment 15:

I'd like to introduce one additional proposal for consideration,
which I believe addresses the original problem brought up by the OP,
and which hasn't been covered yet.
What if "=" was allowed to be used to declare variables as well,
but only if at least one of the variables has *been* previously
declared?
In other words, this would be valid:
   a, err := f()
   if err == nil {
       b, err = g()
       if err == nil { ... }
   }
   return err
This would be the exact counterpart behavior to :=, which may only be
used when at least one of the variables has *not* been declared
previously.  It feels like I'd appreciate using this in practice, and
would avoid the errors I've personally found with the shadowing.
How do you all feel about this?

Comment 16:

Another alternative based on the conversation in the mailing list would be to use a
per-variable declaration syntax.
For instance, this:
   a, b := f()
Would be fully equivalent to:
   :a, :b = f()
and a construction in an internal block such as:
   err = f()
might be extended to the following, which is completely clear and unambiguous:
   :a, err = f()
When one doesn't want to redefine err.
One of the things which feels interesting about this proposal is that
it would enable forbidding entirely partial declarations via := if
that's decided to be a good option, without compromising on other
aspects of the language.

Comment 17 by czapkofan:

Alternative proposals in spirit similar to comment 16, based on ideas expressed in
http://groups.google.com/group/golang-nuts/browse_thread/thread/5f070b3c5f60dbc1 :
Ideas, Variant 1:
  a, (b) := f1()             // redefines b, reuses a
  (a, b), c, (d, e) := f2()  // redefines c, reuses a, b, d, e
  // Flaw example: redundant with "a = f3()":
  (a) := f3()                // reuses a
Ideas, Variant 2:
  (var a), b = f1()           // redefines a, reuses b
  a, b, (var c), d, e = f2()  // redefines c, reuses a, b, d, e
  // Flaw example: redundant with "var a = f4":
  (var a) = f4()              // redefines a

Comment 18 by daveroundy:

I like this approach:
(var a), b = f1()           // redefines a, reuses b
precisely because it is so close to the already-existing equivalence between
a := f1()
and
var a = f1()

Comment 19:

i'm not keen on that, because it's so heavyweight (5 extra characters).
you might as well do
a, nb := f1()
b = nb

Comment 20 by [email protected]:

I won't re-raise this on the list, but after thinking a few more days, I think my
biggest disagreement with the current implementation allowing (the above given):
func TestThree(t *testing.T) {
  if someGlobal, err := someFunc(); err == nil {
    // rather than throwing an error, someGlobal will now silently be an int == 1
  }
  // now it will be a string == "foo" again
}
Is that the part that is creating the issue "if someGlobal, err := someFunc(); err ==
nil" doesn't /really/ seem to be part of the inner block scope to the reader;
Yes, it's completely expected that loop setup variables would be available within the
scope of the loop, and perhaps even, by default, not available outside of the loop
scope.  BUT, since the "clause" is outside of the braces, I think it's reasonable for a
coder to assume that it has a "middle" scope, that would by default inherit from the
global scope if available, otherwise creating variable solely available to the inner
loop scope.
I realize that's a complex description of the change, but I think if /clauses/ are
solely targeted with the change, we'd minimize the chance for both confusion and bugs
unintentionally introduced.
(And if unchanged, I'd love a compiler warning, but hey, I know that's not in the plans
;) )

Comment 21:

James,
"A block is a sequence of declarations and statements within matching brace brackets.
Block = "{" { Statement ";" } "}" . In addition to explicit blocks in the source code,
there are implicit blocks:
   1. The universe block encompasses all Go source text.
   2. Each package has a package block containing all Go source text for that package.
   3. Each file has a file block containing all Go source text in that file.
   4. Each if, for, and switch statement is considered to be in its own implicit block.
   5. Each clause in a switch or select statement acts as an implicit block."
Blocks, The Go Programming Language Specification.
http://golang.org/doc/go_spec.html#Blocks
"In some contexts such as the initializers for if, for, or switch statements, [short
variable declarations] can be used to declare local temporary variables."
Short variable declarations, The Go Programming Language Specification.
http://golang.org/doc/go_spec.html#Short_variable_declarations
Therefore, until you can do it automatically in your head, you can simply explicitly
insert the implicit blocks. For example,
var x = "unum"
func implicit() {
    fmt.Println(x) // x = "unum"
    x := "one"
    fmt.Println(x) // x = "one"
    if x, err := 1, (*int)(nil); err == nil {
        fmt.Println(x) // x = 1
    }
    fmt.Println(x) // x = "one"
}
func explicit() {
    fmt.Println(x) // x = "unum"
    {
        x := "one"
        fmt.Println(x) // x = "one"
        {
            if x, err := 1, (*int)(nil); err == nil {
                fmt.Println(x) // x = 1
            }
        }
        fmt.Println(x) // x = "one"
    }
}

Comment 22 by [email protected]:

Thanks;  It's not so much that I don't understand with it, or even disagree with it; 
It's that it's a frequent source of errors that are hard to physically see (differing
only in colon can have a dramatically different result).
(snip much longer ramble)
I have no problem with
var v; 
func(){ v := 3 }
It's 
foo()(err os.Error){
  for err := bar(); err != nil; err = bar() {
  }
}
being substantially different than
foo()(err os.Error){
  for err = bar(); err != nil; err = bar() {
  }
}
and both being semantically correct.
Essentially, my argument is w/r/t ONLY: "In some contexts such as the initializers for
if, for, or switch statements, [short variable declarations] can be used to declare
local temporary variables";  I would argue that since these are special cases to begin
with, that in multi-variable := usage, resolving those local temporary variables should
be handled via the same scope as the containing block, but stored in the inner scope if
creation is necessary;
I've got no problem with how it works, just been bitten by this more times than I'd care
to admit, and surprised when I'd realized how many others had been as well.

Comment 23 by [email protected]:

I think that Go should be explicit language.
I prefer Go:
    ui = uint(si)
than C:
    ui = si
if ui is unsigned and si is signed. Why do we need an implicit behavior of :=?
So if := is the declaration operator it should work exactly like var for all its lhs.
If some of lhs are previously declared in this scope, it should fail - I believe we
should have a separate explicit construction for this case. Proposal from comment 16 is
nice for me:
    :a, b = f()
In above case it doesn't introduce any additional character. In:
    :a, b, :c = f()
it adds only one.
This notation looks good. I can easily determine what's going on.
    a, b, c := f()
should be an abbreviation for:
    :a, :b, :c = f()
With current := behavior I fill like this:
    :=?
I vote for change this emoticon to:
    :=
in the future.
;)

Comment 24:

In fact I think there is a perfect solution in one of the proposals. So, I'll sum up
what I think:
1. Allow arbitrary addressable expressions on the left side of ':='.
2. Allow no new variables on the left side of ':=' (a matter of consistency in the code,
see examples).
3. Use the following rule to distinguish between a need of "declare and initialize" and
"reuse":
If the LHS looks like an identifier, then the meaning is: "declare and initialize".
Trying to redeclare a variable in the current block that way will issue an error.
Otherwise LHS must be an addressable expression and the meaning is: "reuse". Rule allows
one to use paren expression to trick the compiler into thinking that an identifier is an
addressable expression.
Examples:
a, err := A()   // 'a' and 'err' are identifiers - declare and initialize
b, (err) := B() // 'b' - declare and initialize, '(err)' looks like an addressable
expression - reuse
type MyStruct struct {
    a, b int
}
var x MyStruct
x.a, err := A()   // 'x.a' is an addressable expression - reuse, 'err' is an identifier
- declare and initialize
x.b, (err) := B() // 'x.b' and '(err)' are addressable expressions - reuse (special case
without any new variables)
Of course it could be:
x.b, err = B()    // and that's just a matter of preferrence and consistency
Note: My idea is a bit different from proposal above, the following syntax is invalid: 
(a, b), c := Foo()
The right way to do this is:
(a), (b), c := Foo()
Yes, it's a bit longer. But keep in mind that the alternative is typing 'var a Type',
'var b Type'. Using parens is perfectly fine to me for such a complex case.
Also this approach has one very cool property - it almost doesn't alter syntax (allowing
arbitrary addressable expressions on the left side of ':=' is the only change), only
special semantic meaning.

Comment 25:

I'd still prefer
  :a, :b, c = Foo()
But at this point it's really just syntax.  I'd be happy with either approach.

Comment 26 by ckrueger:

I am in favor of doing away with := entirely because of the desire to control what is
done per-value on multiple returns.  
The :val syntax described above seems nice and short and would seem like valid syntactic
sugar for a keyword driven solution:
:x = f(), declare(shadow) and initialize x, infer type
x  = f(), assign x, infer type
would be the same as
auto var x = f(), declare(shadow) and initialize x, infer type
auto x = f(), assign x, infer type
to revisit the implicit/explicit example shown above in comment 21:
var x = "unum"
func implicit() {
    fmt.Println(x) // x = "unum"
    :x = "one" //<- potentially make this an error, redeclaration after use in same scope.
    //:x = "two" <- would not compile, can only declare once in scope
    fmt.Println(x) // x = "one", global x still = "unum"
    if :x, :err = 1, (*int)(nil); err == nil {
        fmt.Println(x) // x = 1
    }
    fmt.Println(x) // x = "one"
}
func explicit() {
    fmt.Println(x) // x = "unum"
    {
        :x = "one"
        fmt.Println(x) // x = "one"
        {
            if :x, :err = 1, (*int)(nil); err == nil {
                fmt.Println(x) // x = 1
            }
        }
        fmt.Println(x) // x = "one"
    }
    fmt.Println(x) // x = "unum"
}
to revisit the example in the original post:
func f() (err os.Error) {
  :v, err = g(); <-- reusing err for return
  if err != nil {
    return;
  }
  if v {
    :v, err = h(); <-- shadowing v, but reusing err for return
    if err != nil {
      return;
    }
  }
}
in addition, if one wants to enforce typing per-value, specifying type removes the need
for :val as you cannot re-specify a type on an existing value and thus initialisation is
inferred.
int :x, os.Error err = f(); initialize and assign x/error, don't compile if return value
2 is not os.Error

Comment 27:

I think it's safe to say we're not going to change :=.

_Status changed to WorkingAsIntended._

Comment 28 by czapkofan:

Could you possibly elaborate a bit why? Especially with regards to the alternative
"explicit" syntax proposals?
I don't plan to argue, the right to any final decision is obviously yours as always; but
I'd be highly interested to know if there are some problems expected to be introduced by
those proposals, or otherwise what is the common reasoning behind this decision.
Thanks.

Comment 29:

Agreed.  Besides _changing_ :=, there are other proposals, and this problem was brought
up repeatedly in the mailing list by completely different people, with this thread being
referenced as the future answer (how many issues are starred by 46+ people?).
It'd be nice to have some more careful consideration and feedback before dismissal.

Comment 30:

The decision about := is not mine, at least not mine alone.
I am just trying to clean up the bug tracker, so that it reflects
things we need to work on.
1. The bug entry is 1.5 years old at this point.  If it were
going to have an effect, it would have by now.
2. This comes up occasionally on its own.  A bug entry is
not necessary to remind us about it.
I'll change the status back to long term but I remain
skeptical that anything will change.

_Status changed to LongTerm._

Comment 31:

Thanks for the work on cleaning up, it's appreciated. It's also certainly fine for this
to be closed if it reflects a decision made.
The point was mostly that it'd be nice to have some feedback on the features proposed
for solving the problem, given that there's so much interest on the problem and a bit of
love towards a few of the proposed solutions.
E.g. allowing this:
    :v, err = f()
as equivalent to
    var v T
    v, err = f()
If you have internally decided this is off the table, it'd be nice to know it, and if
possible what was the reasoning.

Comment 32:

Variants of this have been discussed in the past,
and they were never compelling enough to make us
want to change anything.
Personally, I think := is working well.

Comment 33:

I don't think magic syntax, such as an extra : on the left hand side, is a good idea.
I think it might be feasible to disallow using := to shadow a variable in an enclosing
block in the same function.  This would have a significant disadvantage: blocks would no
longer stand by themselves.  However, it would remain possible in most cases to write a
block which does stand by itself, by using an explicit "var" instead of :=.  It's just a
thought, I don't know if it really works.

Comment 34:

It's only magic before it's defined and specified. You can think of this:
    v, err := f()
as exactly the same as this:
    :v, :err = f()
Which looks like "distributing the operator".
Some time after we introduce this logic, we can then prevent from *reusing*
variables entirely, and force calls like this:
    v, err := f()
to be made instead as the following when the intention is to reuse a
previous definition of "err":
    :v, err = f()
This, in turn, enables us to consistently use the former/current version of :=
to always mean "shadow/define everything, and error out if the variable isn't
used".
If I'm not missing any details (quite possible) I believe these steps can be
made gradually and in a compatible way, and would address good part of the
reasons why people reach this issue.

Comment 35:

Compatible may not be the right term.. the last change would remove the ambiguity
from :=, so it's of course not compatible. I meant it'd be possible to transition
in a reasonable way.

Comment 36 by gauge2:

As a more novice user, I enjoy the := because it just does what you want it to do. 
Except in this case.  The case where I ran into this involved a global variable not
being set, instead a new local variable was made.
I would like to see an opt out feature of :=, so you can do v, :err := f() in this way
you explicitly say you don't want a redeclaration of the second variable.  This way you
only have to put in a special character for the special cases.
Again I'm a novice user.  I would just like to see a way to use outside scopes, while
still keeping the power of :=

Comment 37 by zexigh:

I agree with Russ: I think ':=' works well.
However, I also once forgot the rules for this operator and got a bug.
Maybe an optional compiler flag could help, by warning the user when
such case happens ? And with an optional compiler flag, gofmt could
automatically rename such variables ?
// --------- example ---------------
package main
import(
    "os"
    "fmt"
)
var i float32
func main() {
    i, err := bar(0) // warning: 'i' hides var defined at ... (global)
    if err != nil {
        i, _ := bar(1) // warning: 'i' hides var defined at ... (local)
        fmt.Println(i)
    }
    fmt.Println(i)
}
func bar(i int) (int, os.Error) {
    if i == 0 {
        return i, fmt.Errorf("blah")
    }
    return i, nil
}

Comment 38 by Nazgand:

I agree with zex. Warnings of hidden variables would be helpful.

Comment 39:

I don't, FWIW.  Shadowing of external variables to me is a benefit rather than a burden.
 I don't really want have to use ok1, ok2, ok3, or i1, i2, i3, or err1, err2, err3.. etc.

Comment 40 by zexigh:

Yeah, that's why I proposed an *optional* flag.

Comment 41:

An optional flag that enables a convention which is entirely unused in the language and
largely not followed by the community doesn't have a place in a standard tool, IMO.
If you follow that convention yourself, though, you can easily build something to verify
your code based on the "go" package.

Comment 42 by zexigh:

Good point. Doing this already.

Comment 43:

_Issue #2283 has been merged into this issue._

Comment 44:

_Labels changed: added priority-later._

Comment 45:

_Labels changed: added priority-someday, removed priority-later._

Comment 46 by 415fox:

Can someone give an example where shadowing a variable name is the right thing to do --
either for clarity, correctness or performance, preferably, all three? Please include
the counter example showing how confusing, broken and burdensome such code would be in a
world without shadowing.
n13m3y3r seems to say in Comment 39 that he has such an example, but my imagination
fails me.

Comment 47:

I think the example might be there in his third sentence.

Comment 48:

Cutting of comments on this issue.  Feel free to discuss on golang-nuts.

_Labels changed: added labelrestrict-addissuecomment-commit._

Comment 49:

_Labels changed: added restrict-addissuecomment-commit, removed labelrestrict-addissuecomment-commit._

Comment 50:

_Issue #4409 has been merged into this issue._

Comment 51:

_Issue #4587 has been merged into this issue._

Comment 52:

_Issue #5990 has been merged into this issue._

Comment 53:

_Issue #5990 has been merged into this issue._

Comment 54:

_Labels changed: added repo-main._

Comment 55:

Adding Release=None to all Priority=Someday bugs.

_Labels changed: added release-none._

Moving priority-someday to the Unplanned milestone.

Summarizing the discussion.

The original problem report was fixed before the Go 1 release by https://golang.org/cl/5245056. That code (if updated to current syntax) now gets an error at compilation time: err is shadowed during return.

Suggestions made in this issue and its duplicates:

  • Don't permit shadowing variables in general.
  • Don't permit shadowing variables declared in the same function (but do permit shadowing global variables).
  • Allow arbitrary expressions on the left hand side of :=, treating them as though they appeared on the left hand side of =.
  • Don't require a new variable declaration on the left hand side of :=.
  • Permit declaring new variables using =, but require at least one of the variables to already exist.
  • Use a per-variable syntax with := to indicate which variables are being declared.

    • Put a : before each variable being declared.

    • Use (var v) for each variable being declared.

  • Don't use an implicit block for := declarations in if, for, switch, since the lack of a { makes them seem to be in the outer block.

FWIW, my suggestion:

  • Permit shadowing with var but not with :=.

Another possibility that may have been mentioned elsewhere:

Currently := permits redeclaring variables that were declared earlier in the same block, as long as at least one of the variables is new. We could change that so that := redeclares variables declared earlier in the same block or in any surrounding block within the same function, as long as at least one of the variables is new.

One thing that seems to be missing from this issue is a clear statement of the problem. For example, is this a problem only for programmers new to Go, or are there cases that are confusing for experienced Go programmers. (The test case that started this issue did seem to be confusing for experienced Go programmers, and that test case no longer compiles.)

Somewhat related to #20802.

I am new to Go; please disregard this feedback if it isn't helpful. The problem, as I see it, is that the behavior of := is overly subtle. Maybe when := was first invented, that was in a simpler context, and it might have been very elegant. But with redeclaration, multiple assignment, and scoping/shadowing effects, it can be unclear what is going on. Again, I am a Go-newbie and am still learning the language, but this is my memory of the problem from several months ago when I looked at it. Also, in reading the proposals above, I hope that changes to := (if any) will make the language simpler rather than more complicated.

I'm a supporter of the colon-prefix syntax proposed above, for example :n, err = f()

@ianlancetaylor has mentioned a per-variable syntax in his summary, but the one I'm talking about is to be used with =, not :=. That is to say, this kind of short declaration would be just a special L-value on the left side of a normal assignment.
I think it could be also fully backward compatible.

Some time ago I wrote a hackish code rewriter for this syntax, and I would be glad if anyone wants to try it out to get a feel of the syntax.
If you do, please read the caveats in the README file that I just jotted down (sorry, I wish I had more time), and keep in mind that I'm publishing this only as a demonstration for this issue.
https://github.com/pam4/gooey

I've used this syntax for some time now, and I find it easier to read and to write, specially in cases similar to the examples reported here and in related issues.

Just got caught by variable shadowing while using filepath.Walk with an anonymous function, because Walk returns an error and the anonymous function has to _accept_ an error, and they're not the same error.

err := filepath.Walk(basedir, func(path string, f os.FileInfo, err error) error { ... })

EDIT: some of the suggestions/solutions summarized above are trying to solve very different problems, and by solving one problem they may make another worse.
I think we also need a list of problems:

1) shadowing is confusing/error-prone (in combination with multi-variable :=, or in general). (suggestions 1, 2, 6)
2) multi-variable := is not informative enough for readers: by just looking at it you can't tell which variables are new and which are being reused; you need to eyeball all the preceding part of the block to get that information (taking into account function arguments and named return values), and you may get confused if you miss something. (suggestion 6)
3) it is not always possible to use a short declaration in a multi-variable context, which partially defeats the convenience of short declarations:
1) when you have at least one variable to declare and at least one arbitrary expression on the left hand side (suggestions 3, 6)
2) when you have at least one variable to declare and at least one variable to reuse from an outer scope (suggestions 5, 6)

(not sure what suggestion 4 in Ian's list is supposed to solve)

I believe that a per-variable short declaration syntax is the only solution that have a chance to solve all the problems I listed (unless you are against shadowing in general).

Proposed per-variable syntax alternatives:
:n, err = f()
n, (err) := f() (details here)
(var n), err = f()
All equivalent to: var n T; n, err = f()

A per-variable syntax could be backward compatible and nearly as terse as a := declaration but would allow finer control, would be more informative, and the compiler would be able to catch more errors.

By introducing special cases about shadowing we would only address problem #1, at the cost of violating the encapsulation notion of a block (as noted here and here): one block could cease to work when its context is changed, even if the block is independent of such context.

Problem #2 cannot be addressed with a per-statement switch (= / :=), no matter what magic you put into it, and problem #3 can be addressed only partially.

Suggestions addressing only problem #3 have generally bad consequences for the other problems, for example allowing bare new variables on the left side of = would just make the lack of explicitness problem worse.

A backwards-compatible approach to shadowing: let var name override shadows within its scope. Although this would not prevent unintended shadows, maybe go vet can catch those...

x, err := fa()
if err == nil {      // OR: if var err; err == nil 
   var err           // override shadowing in if-scope
   y, z, err := fb() // preferable to separate declarations of y & z
}
if err != nil { ... }

To silence go vet re intended shadows, use var name type in the new scope.

x, err := fa()
if err == nil {      // NOT: if var err error; err == nil (valid but wrong)
   var err error     // explicit shadowing; not flagged by go vet
   y, z, err := fb()

Et voila, the scope of err is clear in both cases.

Would like to add a gopher talk illustrating that many people struggle with shadowing including me and hope desperately this will make it in Go2

https://www.youtube.com/watch?v=zPd0Cxzsslk

The most common case in which := is used to redeclare an existing variable, while also defining a new variable, is for the variable err. If we adopt the error handling design draft linked from https://go.googlesource.com/proposal/+/master/design/go2draft.md then it is possible that it will no longer be important to be able to use := to redeclare err. In that case, perhaps we could consider dropping the redeclaration aspect of :=. Instead, all variables on the left hand side of := would be defined for the first time, and if there were already a variable of that name it would be an error. That would be a simple change that might address many people's concerns.

I think that concerns about variable shadowing should be considered separately from := redefinitions, perhaps in a different issue, though I don't know if there is one open for that right now.

I've branched my comment above re variable shadowing to #30321

@ianlancetaylor, there is an old proposal above, which is equivalent* to #30318 plus dropping the redeclaration aspect of := (in fact, #30318 would eliminate the need for redeclarations, regardless of the error handling design).

I'm highly in favor of it: if both of those things were to happen I think all the problems I listed could be solved.

Unfortunately dropping the redeclaration aspect of := is not backward compatible (but #30318 is). On the contrary the colon-prefix syntax is as effective but also backward compatible.

(* actually nsf's proposal differs in its point #2, but it's just a matter of symmetry and doesn't change its effectiveness)

I just got an idea which acts like an inverse of @niemeyer's idea. (_edit: It is more like @nsf's idea_)

The idea removes redeclaration := syntax (which is the different point to the ideas from @niemeyer and @nsf) and tries to solve both the problems mentioned in the current proposal and this one.

The following code shows the logic of the idea:

package bar

func foo() {
   var x, err = f()
   ...
   // Here ":err" means the "err" declared above.
   var y, z, :err = g()
   ...
   {
    // In fact, ":id" means the innermost already declared "id"
    var w, :err = h()
    ...
    // "::id" means the package-level declared "id".
    var u, v, ::err = j()
    ...
    // This "err" is a new declared one.
    var m, n, err = k()
    ...
   }
}

(_edit: the ::id syntax is not essential in this idea._)

Selectors, dereferences, element indexing, and the above mentioned :id/::id forms are allowed to show up in a variable declaration as long as there is at least one pure identifier in the declaration. The pure identifiers represent new declared variables for sure.

var err error

type T struct {
    i *int
    s []int
}
var t T
var p = new(int)

func bar() {
   var *p, t.i, t.s[1], err = 1, p, 2, ::err
   // Yes, the "::id" can be used as sources.
   // ":id" can also but it is a non-sense.
   ...
}

The var keyword can be omitted in the short statements in all kinds of control flows.
For example,

if a, b = c, d; a {
}
//<=>
if var a, b = c, d; a { // some ugly
}

The idea should be Go 1 compatible.

Sorry, the two := in the last code snippet should be both =.

Sorry, the description for control flow short statements is not correct. In fact, the := redeclaration syntax should be kept, but ONLY in the short statements in control flows, just for aesthetics reason. In other words,

if a, b := c, d; a {
}
//<=>
if var a, b = c, d; a { // some ugly
}

The whole idea still keeps Go 1 compatible with this change.

Another simple fix is to prefix *& to re-declared identifiers to indicate the prefixed identifiers are redeclared.

var a, err = A()
var b, *&err = B()

The benefit here is that no new expression notations are invented (same as (err)).

Is *& a nop at runtime, or are you suggesting it should be? It feels like the compiler should be smart enough to figure it out today.

Yes, it has already been a nop now.

Could we consider a clearer (but more verbose) solution to the problem in the same spirit as global keyword in Python which is widely used?

Proposal

  1. A new keyword outerscp is introduced to the language.

    • Usage: outerScp var1, var2, var3

    • It may be used only in the beginning of a scope

    • var1, var2 etc. are variables declared in the outer scope

  2. Whenever a variable name is encountered in the inner scope that has been explicitly specified in the outerscp statement, it is not shadowed
  3. In the absence of this, the standard Go 1 variable shadowing rules apply

Current Go 1 variable shadowing

package main

import (
    "fmt"
)

var a int

func printGlobal() {
    fmt.Printf("Print Global: %d\n", a)
}

func getNum() (int, error) {
    return 4, nil
}

func main() {

    a, err := getNum()
    if err != nil{
        fmt.Printf("error")
        return
    }

    printGlobal()
    fmt.Printf("Printing in main: %d\n", a)
}

Output

Print Global: 0
Printing in main: 4

With outerscp keyword

package main

import (
    "fmt"
)

var a int

func printGlobal() {
    fmt.Printf("Print Global: %d\n", a)
}

func getNum() (int, error) {
    return 4, nil
}

func main() {
        outerscp a

    a, err := getNum()
    if err != nil{
        fmt.Printf("error")
        return
    }

    printGlobal()
    fmt.Printf("Printing in main: %d\n", a)
}

Output

Print Global: 4
Printing in main: 4

Advantages:

  1. Maintains compatibility with Go 1
  2. Explicitly identifies which variables to not be shadowed improving clarity

DisAdvantages:

  1. The outerscp keyword looks ugly. However

    • no public Go code in github uses this term currently as an identifier

    • outerScope keyword is used in several public repositories

    • outer, global etc. are of course used in many repos as identifiers

@srinathh That problem with a keyword like outerscp is that it only helps if you know that you have a problem. If you know that you have a problem, there are other things you can do, like not use :=.

@ianlancetaylor Yes you're right... but that criticism is equally applicable to any other proposal short of completely removing support for the := operator.

I think the key to reducing possibility of subtle bugs is reduce cognitive load so programmers can better maintain contextual awareness. A keyword approach I think makes things a lot more obvious vs. introducing cryptic notations into the := assignment statement and should be familiar at least to Python programmers who form the biggest chunk of Go users who use multiple languages as per the survey.

@srinathh Fair point. I guess what I'm mean is that while cryptic notations are definitely cryptic, and perhaps not a good idea, at least they are short. If I have to write out outerscp v, then I might as well just write var v as needed and stop using :=.

Was this page helpful?
0 / 5 - 0 ratings