-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
Comments
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. |
Issue #514 has been merged into this issue. |
Issue #505 has been merged into this issue. |
Issue #469 has been merged into this issue. |
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 } |
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 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 :-) |
Issue #739 has been merged into this issue. |
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 |
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. |
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 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 ;) ) |
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 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. |
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. ;) |
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. |
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 |
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. |
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. |
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. |
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. |
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. |
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 Of course, the reason that 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. |
@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 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 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. |
Again, the best solution to solve the problem and #30318 is #377 (comment) by @nsf. It is just elegant and makes better consistency. |
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. |
For other people's TLDR purpose, the difference of the two is
vs.
|
@zigo101 — How often do people write functions that return six values? Isn’t doing that a really bad code smell, anyway? |
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 ( So I think it is very unlikely that we would adopt this sort of change. Sorry. This is just my own personal opinion. |
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. |
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 |
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 |
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. |
I'm sorry, but I have to say the cost is much smaller than the semantic change of |
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 |
Although I'm sicking of talking about it, I'm sorry, I only see there are many bad effects of the semantic change of 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 |
So I think it is very unlikely that we would adopt this sort of change.
Sorry.
This is just my own personal opinion.
Does the Go community nowadays have a more formal process of discussion /
voting on proposals that the proposer can attempt?
…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:
***@***.***>
|
@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. |
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. |
A gentle route is to still let the current re-declarations valid in compilation and allow non-names in |
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
…On Tue, 4 Feb 2025 at 1:13 PM, Josh Bleecher Snyder < ***@***.***> wrote:
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.
—
Reply to this email directly, view it on GitHub
<#377 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAWWCFMZDXZSYAIME2ZV2AT2OBEBLAVCNFSM6AAAAABMCXPQBGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMMZSHA4TOOBRGE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@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
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:
Full stop. THAT SAID, I would champion any syntax that would keep me from having add a |
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 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))
} |
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. 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. |
The text was updated successfully, but these errors were encountered: