Skip to content

proposal: spec: various changes to := #377

Open

Description

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

Activity

  1. peterGo commented on Dec 3, 2009

    @peterGo
    Contributor

    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.
  2. gopherbot commented on Dec 9, 2009

    @gopherbot
    Contributor

    Comment 3 by pshah.foss:

    Is there anyway to access the variable in outer scope ?
  3. rsc commented on Jan 11, 2010

    @rsc
    Contributor

    Comment 4:

    Issue #514 has been merged into this issue.

  4. rsc commented on Jan 19, 2010

    @rsc
    Contributor

    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
  5. rsc commented on Jan 19, 2010

    @rsc
    Contributor

    Comment 6:

    Issue #505 has been merged into this issue.

  6. rsc commented on Jan 19, 2010

    @rsc
    Contributor

    Comment 7:

    Issue #469 has been merged into this issue.

  7. gopherbot commented on Feb 5, 2010

    @gopherbot
    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
    }
  8. rsc commented on Feb 5, 2010

    @rsc
    Contributor

    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.
  9. rogpeppe commented on Mar 26, 2010

    @rogpeppe
    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.
  10. gopherbot commented on Apr 21, 2010

    @gopherbot
    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  :-)
  11. rsc commented on Apr 26, 2010

    @rsc
    Contributor

    Comment 12:

    Issue #739 has been merged into this issue.

  12. gopherbot commented on Nov 20, 2010

    @gopherbot
    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
  13. mark-summerfield commented on Nov 20, 2010

    @mark-summerfield

    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.)
  14. niemeyer commented on Nov 23, 2010

    @niemeyer
    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?
  15. niemeyer commented on Nov 30, 2010

    @niemeyer
    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.
  16. 112 remaining items

  17. zigo101 commented on Feb 3, 2025

    @zigo101
    Contributor

    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.

  18. ianlancetaylor commented on Feb 3, 2025

    @ianlancetaylor
    Contributor

    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.

  19. zigo101 commented on Feb 4, 2025

    @zigo101
    Contributor

    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.

  20. srinathh commented on Feb 4, 2025

    @srinathh
    Contributor
  21. ianlancetaylor commented on Feb 4, 2025

    @ianlancetaylor
    Contributor

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

  22. josharian commented on Feb 4, 2025

    @josharian
    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.

  23. zigo101 commented on Feb 4, 2025

    @zigo101
    Contributor

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

  24. srinathh commented on Feb 4, 2025

    @srinathh
    Contributor
  25. mikeschinkel commented on Feb 5, 2025

    @mikeschinkel

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

  26. mikeschinkel commented on Feb 5, 2025

    @mikeschinkel

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

    Linking my reply, for anyone interested in reading it.

  27. zigo101 commented on Feb 5, 2025

    @zigo101
    Contributor

    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))
    }
  28. mikeschinkel commented on Feb 5, 2025

    @mikeschinkel

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

  29. benhoyt commented on Jan 29, 2026

    @benhoyt
    Contributor

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

  30. rostidev commented on Jun 15, 2026

    @rostidev

    I think we must keep the := backward compatible. With this backward compatibility in mind I think about two possible solutions:

    1. 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()
    1. 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.
    var val []byte
    
    for range 3 {
        // 'val' is assigned (outer scope), 'err' is declared (local scope)
        val, err ::= foo() 
        if err != nil {
            return err
        }
    }
  31. zigo101 commented on Jun 15, 2026

    @zigo101
    Contributor

    Your first possible solution is exactly #377 (comment), which doesn't introduce new syntax.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions