You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.)
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.
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
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
}
@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.
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.
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 :-)
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
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.)
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?
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.
Reacted by tandr, BrianWill, pam4, Rodrigo, DeedleFake, bokwoon95, Bogdan Petrea, Michal Bohuslávek, Collin Van Dyck, bl-ue and 3 more
I'm sorry, but I have to say the cost is much smaller than the semantic change of for loops.
The change makes some code not compile, which is an explicit way (so it is good).
On the other hand, the for-loop semantic change doesn't make old code blocks fail to compile,
but make their behavior changed and some of the behavior changes are not easily to detect.
As you know, the for loop change was an exceptional case that we made after a lot of thought. It had a large cost and a large benefit, and we decided that the benefit outweighed the cost.
We can argue over whether the := change has a larger cost or not. It doesn't really matter. What matters is that, in my opinion, the benefit of the := change is not nearly as large as for the loop change, and in particular the benefit doesn't outweigh the cost.
Although I'm sicking of talking about it, I'm sorry, I only see there are many bad effects of the semantic change of for;; loops and see tiny benefits of it. (NOTE: I agree that the benefits of the semantic change of for-range loops are much larger than its cost).
I don't understand why breaking code behavior and not providing a way to detect all of the behavior changes will be suppressed the so called `benefits". And up to now, I still don't see there is any effort (and even the willing) to provide a way to detect all of the behavior changes.
IMHO, the semantic change damaged Go's reputation for promoting explicitness and maintaining strong backward compatibility.
As for the := change, at least there are only the cost of invalidating most existing documentation. And I don't think the cost is that large. I don't think changing millions of lines of Go code is a cost.
Reacted by inliquid
Reacted by Sean Liao, Tom Thorogood, Carlana and Florian Lehner
On Tue, 4 Feb 2025, 4:19 am Ian Lance Taylor, ***@***.***> wrote:
Copying the comment I just added on #71522
<#71522> :
I appreciate that you have put a lot of work into this proposal.
However, I see no realistic way that we are going to add a new declaration
syntax at present. We already have two (var and :=). You are suggesting a
third, which is to permit = on the left. This makes the language more
complicated. I'm not familiar with any other language that uses a similar
syntax, so I think it will be a barrier for people new to Go who are
reading Go code. The impetus for the change appears to be easier
refactoring, but that is not in itself a particularly strong argument:
refactoring should not result in code that is harder to understand.
So I think it is very unlikely that we would adopt this sort of change.
Sorry.
This is just my own personal opinion.
—
Reply to this email directly, view it on GitHub
<#377 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAWWCFOAZUNWNGKSZBCRAVL2N7FLXAVCNFSM6AAAAABMCXPQBGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMMZRHE4DMMZYGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
A gentle route is to still let the current re-declarations valid in compilation and allow non-names in := statements, but let go vet warn on them and suggest to use go fix to modify them. (Or it would be better to let go fmt modify them?)
Yes I’m aware. I recall how dependency management happened :-) . I was
wondering if the language governance and decisioning process has evolved
since towards broader community participation/voting.
Best Regards
Hariharan Srinath
How often do people write functions that return six values?
Isn’t doing that a really bad code smell, anyway?
I'm surprised by your focus point. It is just a demonstration of all possible L-value forms.
It is a focus because when six (6) assignments are shown it appears the parenthesis syntax is less cluttered.
However, if we were instead of show each one with two (2) return values — which is far more common — it makes the parenthesis syntax appear obtuse and confusing, since now := is reassigning, but only for non-references, and () affects assignment how?
"your proposal introduces new syntax, but #377 (comment) doesn't."
How does it not propose new syntax? You cannot currently enclose a LHS variable in parenthesis:
(oldDeclared) :=doSomething()
BTW, that comment presents a set of rules that are so complex that, try-as-I-might, I could still not figure out what it was trying to say in its entirety. Conversely, my proposal is simple:
Use := to declare and assign variables per the existing rules of :=, and
Use = to only assign per the existing rules of =.
Full stop.
THAT SAID, I would champion any syntax that would keep me from having add a var declaration for a variable that was previously declared with := because therein lies the progenitor of unintended bugs.
How does it not propose new syntax? You cannot currently enclose a LHS variable in parenthesis:
(oldDeclared) := doSomething()
Yes, absolutely true. That is what is the meaningfulness of #377 (comment) and #30318. (NOTE: it might require that there must be at least one new-declared variable in a := statement)
Why it is not a new syatax is because the following code is valid:
"How does it not propose new syntax? You cannot currently enclose a LHS variable in parenthesis: (oldDeclared) := doSomething()"
"It is not a new syatax is because the following code is valid:"
Well color me corrected then.
OTOH I have never come across that, had no idea it was valid, would be surprised if many others knew it is valid, and most importantly. (oldDeclared) := 2 is not valid and does not currently have any syntactical effect, let alone the one you are proposing. Talk about violating the principle of least surprise.
That said, again, I would happily embrace that syntaxif it were the only way Go would provide to handle both newly declared and not-previously declared variables on the LHS in a single assignment statement. Though clearly, if I had my choice, I would not pick that syntax.
@ianlancetaylor or other core Go devs: does it make sense to just close this issue? It seems to me that there's no way we're going to change the semantics of := this late in the game.
I think we must keep the := backward compatible. With this backward compatibility in mind I think about two possible solutions:
Possibility to mark somehow the variables that we don't want to shadow. Just a few possible examples (there could be other options as well):
::val, err:=foo() // looks similar to C++ but means val from the closest outside scope and not directly from the global scope
^val, err:=foo()
{val}, err:=foo()
@val, err:=foo()
.val, err:=foo()
Add another ::= operator that works with a mixed logic of = and := together, i.e. it never shadows outer and global variables but always creates new local variables that don't exist in outer or global scope.
Activity
peterGo commentedon Dec 3, 2009
Comment 1:
gopherbot commentedon Dec 9, 2009
Comment 3 by pshah.foss:
rsc commentedon Jan 11, 2010
Comment 4:
Issue #514 has been merged into this issue.
rsc commentedon Jan 19, 2010
Comment 5:
rsc commentedon Jan 19, 2010
Comment 6:
Issue #505 has been merged into this issue.
rsc commentedon Jan 19, 2010
Comment 7:
Issue #469 has been merged into this issue.
gopherbot commentedon Feb 5, 2010
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 }rsc commentedon Feb 5, 2010
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.rogpeppe commentedon Mar 26, 2010
Comment 10:
gopherbot commentedon Apr 21, 2010
Comment 11 by ravenstone13@cox.net:
rsc commentedon Apr 26, 2010
Comment 12:
Issue #739 has been merged into this issue.
gopherbot commentedon Nov 20, 2010
Comment 13 by snake.scaly:
mark-summerfield commentedon Nov 20, 2010
Comment 14:
niemeyer commentedon Nov 23, 2010
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?niemeyer commentedon Nov 30, 2010
Comment 16:
112 remaining items
zigo101 commentedon Feb 3, 2025
I'm sorry, but I have to say the cost is much smaller than the semantic change of
forloops.The change makes some code not compile, which is an explicit way (so it is good).
On the other hand, the
for-loop semantic change doesn't make old code blocks fail to compile,but make their behavior changed and some of the behavior changes are not easily to detect.
ianlancetaylor commentedon Feb 3, 2025
As you know, the for loop change was an exceptional case that we made after a lot of thought. It had a large cost and a large benefit, and we decided that the benefit outweighed the cost.
We can argue over whether the
:=change has a larger cost or not. It doesn't really matter. What matters is that, in my opinion, the benefit of the:=change is not nearly as large as for the loop change, and in particular the benefit doesn't outweigh the cost.zigo101 commentedon Feb 4, 2025
Although I'm sicking of talking about it, I'm sorry, I only see there are many bad effects of the semantic change of
for;;loops and see tiny benefits of it. (NOTE: I agree that the benefits of the semantic change offor-rangeloops are much larger than its cost).I don't understand why breaking code behavior and not providing a way to detect all of the behavior changes will be suppressed the so called `benefits". And up to now, I still don't see there is any effort (and even the willing) to provide a way to detect all of the behavior changes.
IMHO, the semantic change damaged Go's reputation for promoting explicitness and maintaining strong backward compatibility.
As for the
:=change, at least there are only the cost of invalidating most existing documentation. And I don't think the cost is that large. I don't thinkchanging millions of lines of Go codeis a cost.srinathh commentedon Feb 4, 2025
ianlancetaylor commentedon Feb 4, 2025
@zigo101 We simply disagree.
@srinathh The formal proposal process is described at https://go.dev/s/proposal. Note that I am on the proposal review committee.
josharian commentedon Feb 4, 2025
As someone who has watched a decade of Go proposals from the outside, I would second Ian's assessment of the likeliness of this changing.
zigo101 commentedon Feb 4, 2025
A gentle route is to still let the current re-declarations valid in compilation and allow non-names in
:=statements, but letgo vetwarn on them and suggest to usego fixto modify them. (Or it would be better to letgo fmtmodify them?)srinathh commentedon Feb 4, 2025
mikeschinkel commentedon Feb 5, 2025
@zigo101 —
It is a focus because when six (6) assignments are shown it appears the parenthesis syntax is less cluttered.
However, if we were instead of show each one with two (2) return values — which is far more common — it makes the parenthesis syntax appear obtuse and confusing, since now
:=is reassigning, but only for non-references, and()affects assignment how?How does it not propose new syntax? You cannot currently enclose a LHS variable in parenthesis:
BTW, that comment presents a set of rules that are so complex that, try-as-I-might, I could still not figure out what it was trying to say in its entirety. Conversely, my proposal is simple:
:=to declare and assign variables per the existing rules of:=, and=to only assign per the existing rules of=.Full stop.
THAT SAID, I would champion any syntax that would keep me from having add a
vardeclaration for a variable that was previously declared with:=because therein lies the progenitor of unintended bugs.mikeschinkel commentedon Feb 5, 2025
Linking my reply, for anyone interested in reading it.
zigo101 commentedon Feb 5, 2025
Yes, absolutely true. That is what is the meaningfulness of #377 (comment) and #30318. (NOTE: it might require that there must be at least one new-declared variable in a
:=statement)Why it is not a new syatax is because the following code is valid:
mikeschinkel commentedon Feb 5, 2025
Well color me corrected then.
OTOH I have never come across that, had no idea it was valid, would be surprised if many others knew it is valid, and most importantly.
(oldDeclared) := 2is not valid and does not currently have any syntactical effect, let alone the one you are proposing. Talk about violating the principle of least surprise.That said, again, I would happily embrace that syntax if it were the only way Go would provide to handle both newly declared and not-previously declared variables on the LHS in a single assignment statement. Though clearly, if I had my choice, I would not pick that syntax.
benhoyt commentedon Jan 29, 2026
@ianlancetaylor or other core Go devs: does it make sense to just close this issue? It seems to me that there's no way we're going to change the semantics of
:=this late in the game.rostidev commentedon Jun 15, 2026
I think we must keep the
:=backward compatible. With this backward compatibility in mind I think about two possible solutions:{val}, err := foo()::=operator that works with a mixed logic of=and:=together, i.e. it never shadows outer and global variables but always creates new local variables that don't exist in outer or global scope.zigo101 commentedon Jun 15, 2026
Your first possible solution is exactly #377 (comment), which doesn't introduce new syntax.