Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: spec: various changes to := #377

Open
agl opened this issue Dec 3, 2009 · 116 comments
Open

proposal: spec: various changes to := #377

agl opened this issue Dec 3, 2009 · 116 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@agl
Copy link
Contributor

agl commented Dec 3, 2009

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.)
@peterGo
Copy link
Contributor

peterGo commented Dec 3, 2009

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.

@gopherbot
Copy link
Contributor

Comment 3 by pshah.foss:

Is there anyway to access the variable in outer scope ?

@rsc
Copy link
Contributor

rsc commented Jan 11, 2010

Comment 4:

Issue #514 has been merged into this issue.

@rsc
Copy link
Contributor

rsc commented Jan 19, 2010

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

@rsc
Copy link
Contributor

rsc commented Jan 19, 2010

Comment 6:

Issue #505 has been merged into this issue.

@rsc
Copy link
Contributor

rsc commented Jan 19, 2010

Comment 7:

Issue #469 has been merged into this issue.

@gopherbot
Copy link
Contributor

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
Copy link
Contributor

rsc commented 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
Copy link
Contributor

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.

@gopherbot
Copy link
Contributor

Comment 11 by ravenstone13@cox.net:

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  :-)

@rsc
Copy link
Contributor

rsc commented Apr 26, 2010

Comment 12:

Issue #739 has been merged into this issue.

@gopherbot
Copy link
Contributor

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

@mark-summerfield
Copy link

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.)

@niemeyer
Copy link
Contributor

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
Copy link
Contributor

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.

@gopherbot
Copy link
Contributor

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

@gopherbot
Copy link
Contributor

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()

@rogpeppe
Copy link
Contributor

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

@gopherbot
Copy link
Contributor

Comment 20 by james@abneptis.com:

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
;) )

@peterGo
Copy link
Contributor

peterGo commented Dec 11, 2010

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"
    }
}

@gopherbot
Copy link
Contributor

Comment 22 by james@abneptis.com:

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.

@gopherbot
Copy link
Contributor

Comment 23 by ziutek@Lnet.pl:

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.
;)

@nsf
Copy link

nsf commented Apr 6, 2011

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.

@niemeyer
Copy link
Contributor

niemeyer commented Apr 6, 2011

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.

@gopherbot
Copy link
Contributor

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

@rsc
Copy link
Contributor

rsc commented Jul 25, 2011

Comment 27:

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

Status changed to WorkingAsIntended.

@gopherbot
Copy link
Contributor

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.

@niemeyer
Copy link
Contributor

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.

@rsc
Copy link
Contributor

rsc commented Jul 26, 2011

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.

@niemeyer
Copy link
Contributor

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.

@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Aug 6, 2024
@mikeschinkel
Copy link

mikeschinkel commented Feb 1, 2025

This issue — opened in 2009 — appears to be the issue where any assignment-related tickets give tagged as a duplicate of — such as #70337.

However, AFAICT there is no fleshed-out proposal here to consider — or at least not one that is still relevant given the Go compatibility guarantee — and as such it appears that discussions have meandered around for years but with nothing objective to accept or reject.

Further, since the vast majority of discussion on this ticket occurred before the Go team decided that there would be no version 2.0 that allowed breaking changes, most of the discussion has been in that context and is thus obsolete.

Given both of those facts it feels like tagging other tickets as duplicates of this ticket result in condemning them to purgatory without a fair and honest rejection of their nuances on their own merits.

Respectfully I'd like to request this 16 year old ticket be closed and that a new ticket which summarizes the considerations that the Go team is willing to address related to this ticket be created and that that ticket have a proposal that can be objectively accepted or declined.

In addition, it would be great to have all the tickets closed as duplicates of this one that are still potentially relevant — and ones that were closed as duplicates of the ones closed as a duplicate of this one — to be mentioned on this new ticket.

Thank you in advance for the consideration.

@ianlancetaylor
Copy link
Member

A short response is that I think it's extremely unlikely that we will make any change to the current behavior of variable declarations. We aren't going to remove shadowing: all Algol-derived languages permit variable shadowing in blocks, and it permits moving blocks from one function to another without surprises. We aren't going to introduce new variable declaration syntaxes: we already have two, and we really don't want more.

The only improvement I personally could imagine making in this area would be removing the ability to redeclare variables using :=. That is, require that when := is used, all variables on the left must be new. I believe that that simple change would remove a lot, perhaps all, of the current confusion. That change would of course not be backward compatible, but it would not break the cardinal rule of compatibility, which is that it must not be the case that a valid Go program change behavior. It would change currently working Go programs into Go programs that no longer work. That is a change that we could theoretically make over time with a lot of preparatory work.

Of course, the reason that := permits redeclaration is the err variable of type error. So the only way that we could make this change would be to first find some way to avoid the requirement to frequently repeat the err variable. And at this point, despite many attempts, that seems fairly unlikely.

So I expect that nothing will happen in this area. It's not the best part of Go, but there are other problems that are significantly worse in practice.

To be clear, this is all just my personal opinion.

@mikeschinkel
Copy link

@ianlancetaylor — Thank you very much for taking the time to make a significant reply.

What caused me to ask was I spent literally all day Saturday writing up a proposal that I think addresses all your concerns and is 100% backward compatible only to have it closed within a few minutes of opening as a duplicate of #21303 which itself had been closed as a duplicate of this issue (#377) almost 7 years ago.

So, would it be too much to ask you to quickly scan my proposal to see if it does not indeed address all concerns in a backward compatible manner? And if not, provide an explanation as to why?

In a nutshell, it proposes allowing the mixing of := and = operators. The following presumes response is new but err has previously been declared and in this case we want to shadow it:

response =, err := client.Do(req)  // response exists, err is new

I wrote a lot of supporting evidence in my closed issue as to how this improves the refactoring use-case as that was my primary motivation for writing it up. Having to constantly make multi-line changes during refactoring and being forced to name the type in a var statement when I did not before the refactoring occured is the pain point that has had me planning to propose an improvement for several years.

If after reviewing it you feel it is not viable, I will accept that. I simply want someone in a position to generate an opinion for the team to at least take a look at it given how much effort I put into it.

@zigo101
Copy link

zigo101 commented Feb 3, 2025

Again, the best solution to solve the problem and #30318 is #377 (comment) by @nsf. It is just elegant and makes better consistency.

@srinathh
Copy link
Contributor

srinathh commented Feb 3, 2025

Actually I rather like this new proposal from @mikeschinkel . It appears minimalist, elegant and backwards compatible. While I'm no longer as active in the Go community, I do suggest this be looked at as shadowing errors have been a pain.

#71522

@zigo101
Copy link

zigo101 commented Feb 3, 2025

For other people's TLDR purpose, the difference of the two is

#71522

newDeclared :=, oldDeclared =, *aPointer =, aStruct.x =, aSlice[i] =, aMap[k] =  secondOperation()

vs.

#377 (comment)

newDeclared, (oldDeclared), *aPointer, aStruct.x, aSlice[i], aMap[k] := secondOperation()

@mikeschinkel
Copy link

mikeschinkel commented Feb 3, 2025

@zigo101 — How often do people write functions that return six values?

Isn’t doing that a really bad code smell, anyway?

@ianlancetaylor
Copy link
Member

Copying the comment I just added on #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.

@zigo101
Copy link

zigo101 commented Feb 3, 2025

@mikeschinkel

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.

Please note that, as @ianlancetaylor has mentionsed, your proposal introduces new syntax, but #377 (comment) doesn't.

@ianlancetaylor
Copy link
Member

But if I understand #377 (comment) correctly, it is not backward compatible. We would have to change a large amount of existing code to add parentheses around err and other redeclared variables.

@zigo101
Copy link

zigo101 commented Feb 3, 2025

If #377 (comment) is adopted since a Go version, then any re-declarations will not compile. (Is it right? I'm not very sure about this, but it seems right), then just use go fix to wrap them in ().

@ianlancetaylor
Copy link
Member

Changing millions of lines of Go code and invalidating most existing documentation is a very large cost. We could only do that if there were a correspondingly large benefit.

@zigo101
Copy link

zigo101 commented Feb 3, 2025

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.

@ianlancetaylor
Copy link
Member

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
Copy link

zigo101 commented 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 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.

@srinathh
Copy link
Contributor

srinathh commented Feb 4, 2025 via email

@ianlancetaylor
Copy link
Member

@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
Copy link
Contributor

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
Copy link

zigo101 commented Feb 4, 2025

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?)

@srinathh
Copy link
Contributor

srinathh commented Feb 4, 2025 via email

@mikeschinkel
Copy link

@zigo101 — 

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?

newDeclared :=, oldDeclared = doSomething()
newDeclared, (oldDeclared) := doSomething()
newDeclared :=, *aPointer = doSomething()
newDeclared, *aPointer := doSomething()
newDeclared :=, aStruct.x = doSomething()
newDeclared, aStruct.x := doSomething()
newDeclared :=, aSlice[i] = doSomething()
newDeclared, aSlice[i] := doSomething()
newDeclared :=, aMap[k] = doSomething()
newDeclared, aMap[k] := doSomething()

"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:

  1. Use := to declare and assign variables per the existing rules of :=, and
  2. 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.

@mikeschinkel
Copy link

ianlancetaylor: "Copying the comment I just added on #71522:"

Linking my reply, for anyone interested in reading it.

@zigo101
Copy link

zigo101 commented Feb 5, 2025

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:

package main

func main() {
	var a, b = 1, 2
	(a), (b) = 8, 9
	println((a), (b))
}

@mikeschinkel
Copy link

"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 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests