That's fundamentally very weird from any systems background, and suggests a language design error: if people are returning pointers to "locals" because returning by value does an implicit copy, then maybe it shouldn't have been specified to do that.
That's fundamentally very weird from any systems background, and suggests a language design error: if people are returning pointers to "locals" because returning by value does an implicit copy, then maybe it shouldn't have been specified to do that.
That the runtime can allocate objects on the stack instead is an optimization, not a part of the language semantics. Letting the reference escape the enclosing function does not "automatically promote the object to the heap", it simply prevents the optimization above.
Also, none of this has anything to do with the problem of taking the address of loop variables. The confusing thing there is not the lifetime of the loop variable per se. It is the fact that the range-based loop variable is mutated to contain a copy of the next element in the range at the next iteration. It doesn't work like the C++ range-based loop, where the range_declaration is considered part of the loop body, for example.
Every other language I can think of that does this spells it in a materially different way, kind of like this (intentionally not matching any particular language):
x = func();
y = x;
Now x and y are references to the same thing, each one is sufficient to keep the referent alive, and they are on equal footing.This includes every GC language I’m familiar with except Go. It also includes C++ when using any sort of pointer (including smart pointers) and even plain references (with the obvious caveats about returning a reference to a local variable being nonsense in C++). It includes Swift with ARC. And it includes functional languages, pure and otherwise. If you return a (correct) reference to an object that would otherwise be local, you are returning the same type as the original local variable.
I think go is unusual in that it looks more like this:
x = func();
y = &x:
Here x has type int (or whatever). y has type int. Yet x keeps that integer alive, and so does y. x and y refer to the same thing. &x and &y are not equivalent.So I think woodruffw is reasonable to find returning the address of a local variable weird coming from pretty much any background.
However, similar things can be done in C# for example by capturing a local variable in a closure, even if that variable is of a value type (struct). The closure then becomes something similar to a Go reference to that value, and you are still free to return it with no UB or other problems.
Either way, if your mental model is adjusted to the GC memory model, there is nothing special going on here. The weirdness comes only out of tying an object's lifetime to its scope. Scope is irrelevant to lifetime in GC languages, where lifetime is tied to (live) references to that object. So, if a reference to an object "escapes" its scope, that object will live beyond the scope where it was originally declared - nothing strange about that.
Whether you agree or not, I definitely think that promoting loop variables to be heap allocated is consistent with the rest of Go.
(Obviously there are some consequences to this, wherein you may find yourself with heap-based allocation in hot paths that you didn't expect. But in terms of productivity, I think that not caring and having no distinction certainly reduces one thing that the programmer has to think about.)
I think there would be analogous design decisions that remove various low-level concepts in many languages. For example, Python ints are arbitrary precision, which removes a whole set of concerns from the programmer. Rather than being "weird", I think this is just optimizing for a particular kind of programmer productivity.