Hacker News new | past | comments | ask | show | jobs | submit login

Technically there is no crazy behaviour, it’s a natural consequence of scoping loop variables outside the loop, which historically was common.

It became an issue as lambdas and other lambda-type constructs (which implicitly keep a reference on the loop variable) became more common, and a bunch of languages got caught in it. Later languages switched to the “inner scoping” mechanism to avoid it.

Go did not follow the switch because Go.




This is not really what's happening. The variable is scoped to the loop. The issue is that even if you take the address of the variable, each iteration of the loop will have the same variable with the same address.

    nums := []int{1, 2, 3}
    for _, num := range nums {
        fmt.Printf("%p\n", &num)
    }
This will print the same address three times. If you add "num := num" as the first line in the loop, it will print three different addresses. The proposal is to make this the default behaviour.


> This is not really what's happening.

Yes it is.

> The variable is scoped to the loop.

Maybe “loop body” would make the comment clearer?

> The issue is that even if you take the address of the variable, each iteration of the loop will have the same variable with the same address.

Hence the variable not being scoped to the loop (body), one variable is shared between all iterations of the loop.


> Maybe “loop body” would make the comment clearer?

I think "loop iteration" might.


Arguably not, because an 'iteration' is a unit of execution, not a lexical unit. If the variables were really scoped to loop iterations, that would be a form of dynamic scope, which would have a different semantics. So for example, say the loop calls a function foo. This function executes inside every iteration of the loop, but within foo, one cannot access the loop iteration variable (as it is in a different lexical context).


My point is precisely that lexical terms (like loop body) are insufficient to explain the Go behaviour. The loop variable is clearly lexically scoped to the loop body. "Scoping loop variables outside the loop" is commonly understood as the Python gotcha:

    for x in range(3):
        foo(x)
    print(x)  # prints 2
or the pre-C99 style:

    int i;
    for (i = 0; i < 3; ++i)
        foo(i)
    printf("%d", i);  // prints 3
This is not the case in Go. I don't think talking about variable scopes accurately describes the issue (because there's nothing special about loop scopes here: the same "escape" can happen from any scope), and changing "loop" to "loop body" doesn't improve this. The term "loop iteration" at least identifies the dependency between different iterations of the loop as the issue.

I don't understand what the thought experiment about non-lexical scopes has to do with this.


It's purely a question of scopes, as indicated by the equivalent code in the article:

    for _, elem := range elems {
      elem := elem
      ... &elem ...
    }
Nothing beyond regular lexical scoping and Go's ordinary assignment semantics are necessary to see how this works. The second 'elem' has a narrower scope than the first (it is limited to the loop body). Abusing Go syntax, you can think of the current semantics as follows:

    {
      var elem Elem
      for _, elem = range elems {
        ... &elem ...
      }
    }
Here 'elem' scopes outside the loop body, and so is reassigned on every iteration of the loop (and &elem evaluates to the same address on every iteration).

>the thought experiment about non-lexical scopes

It's not just a thought experiment. There are languages with dynamically scoped variables (e.g. global vars in Common Lisp).


I understood the JS version of this problem, but to see it happen with an address-of operator is just weird. In C if I took the address of a function scoped / temporary variable and kept it around it would be very bad.

I guess taking the address "promoted" the shared variable to enable it to survive past the function?


Yes, if you take a reference and it “escapes” the variable gets boxed (promoted to a pointer on the heap).


Said differently: Go puts variables on the stack only when the compiler can prove no references escape the stack frame. Stack is purely an optimization.


Except

1. Java is also supposed to do that, and clearly way less successful

2. Escape log is part of the standard baseline tools (it’s just a flag), so it’s not considered anything hidden or arcane

3. Go tends to log escapes, meaning it’s baseline is to stack allocate

So it’s a lot more than, say, common subexpression optimisation.


The subtle difference between the two ways of communicating the design: There are cases where the reference does not escape, but the compiler doesn't know how to prove that. So saying "if it escapes it's boxed" is subtly wrong.

The default is heap, and only when the compiler can be sure it's safe, things go on the stack.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: