In my experience, the average programmer isn’t even aware of the stack vs heap distinction these days. If you learned to write code in something like Python then coming at Go from “above” this will just work the way you expect.
If you come at Go from “below” then yeah it’s a bit weird.
That said, when it matters it matters a lot. In those times I wish it was more visible in Go code, but I would want it to not get in the way the rest of the time. But I’m ok with the status quo of hunting down my notes on escape analysis every few months and taking a few minutes to get reacquainted.
Side note: I love how you used “from above” and “from below”. It makes me feel angelic as somebody who came from above; even if Java and Ruby hardly seemed like heaven.
I also came "from above".
Back in Python 2.1 days, there was no guarantee that a locally scoped variable would continue to exist past the end of the method. It was not guaranteed to vanish or go fully out of scope, but you could not rely on it being available afterwards. I remember this changing from 2.3 onwards (because we relied on the behaviour at work) - from that point onwards you could reliably "catch" and reuse a variable after the scope it was declared in had ended, and the runtime would ensure that the "second use" maintained the reference count correctly. GC did not get in the way or concurrently disappear the variable from underneath you anymore.
Then from 2008 onwards the same stability was extended to more complex data types. Again, I remember this from having work code give me headaches for yanking supposedly out-of-scope variable into thin air, and the only difference being a .1 version difference between the work laptop (where things worked as you'd expect) and the target SoC device (where they didn't).
even in C, the concept of returning a pointer to a stack allocated variable is explicitly considered undefined behavior (not illegal, explicitly undefined by the standard, and yes that means unsafe to use). It be one thing if the the standard disallowed it.
but that's only because the memory location pointed to by the pointer will be unknown (even perhaps immediately). the returning of the variable's value itself worked fine. In fact, one can return a stack allocated struct just fine.
TLDR: I don't see what the difference between returning a stack allocated struct in C and a stack allocated slice in Go is to a C programmer. (my guess is that the C programmer thinks that a stack allocated slice in Go is a pointer to a slice, when it isn't, it's a "struct" that wraps a pointer)
The following Go code also works perfectly well, where it would obviously be UB in C:
func foo() *int {
i := 7
return &i
}
func main() {
x := foo()
fmt.Printf("The int was: %d", *x) //guaranteed to print 7
}Why? It is the same as in C.
#include <stdio.h>
#include <stdlib.h>
struct slice {
int *data;
size_t len;
size_t cap;
};
struct slice readLogsFromPartition() {
int *data = malloc(2);
data[0] = 1;
data[1] = 2;
return (struct slice){ data, 2, 2 };
}
int main() {
struct slice s = readLogsFromPartition();
for (int i = 0; i < s.len; i++) {
printf("%d\n", s.data[i]);
}
free(s.data);
} func foo() {
x := []int { 1 }
//SNIP
}
Could translate to C either as: void foo() {
int* x = malloc(1 * sizeof(int));
x[0] = 1;
//...
}
Or as void foo() {
int data[1] = {1};
int *x = data;
//...
}
Depending on the content of //SNIP. However, some people think that the semantics can also match the semantics of the second version in C - when in fact the semantics of the Go code always match the first version, even when the actual implementation is the second version. int *foo(void) {
int x = 99;
return &x; // bad idea
}
vs. func foo() *int {
x := 99
return &x // fine
}
They think that Go, like C, will allocate x on the stack, and that returning a pointer to the value will therefore be invalid.(Pedants: I'm aware that the official distinction in C is between automatic and non-automatic storage.)
Assuming to everything allocates on the heap, will solve this specific confusion.
My understanding is that C will let you crash quite fast if the stack becomes too large, go will dynamically grow the stack as needed. So it's possible to think you're working on the heap, but you are actually threshing the runtime with expensive stack grow calls. Go certainly tries to be smart about it with various strategies, but a rapid stack grow rate will have it's cost.
A straightforward reading of the code suggests that it should do what it does.
The confusion here is a property of C, not of Go. It's a property of C that you need to care about the difference between the stack and the heap, it's not a general fact about programming. I don't think Go is doing anything confusing.
But yeah, to your point, returning a slice in a GC language is not some exotic thing.
I commented elsewhere on this post that I rarely have to think about stacks and heaps when writing Go, so maybe this isn’t my issue to care about either.
Sure, Go has escape analysis, but is that really what's happening here?
Isn't this a better example of escape analysis: https://go.dev/play/p/qX4aWnnwQV2 (the object retains its address, always on the heap, in both caller and callee)
Both arrays in this example seem to be on the heap.
Since 1.17 it’s not impossible for escape analysis to come into play for slices but afaik that is only a consideration for slices with a statically known size under 64KiB.
The thing being returned is a slice (a fat pointer) that has pointer, length, capacity. In the code linked you'll see the fat pointer being returned from the function as values. in C you'd get just AX (the pointer, without length and cap)
command-line-arguments_readLogsFromPartition_pc122:
MOVQ BX, AX // slice.ptr -> AX (first result register)
MOVQ SI, BX // slice.len -> BX (second)
MOVQ DX, CX // slice.cap -> CX (third)
The gargabe collection is happening in the FUNCDATA/PCDATA annotations, but I don't really know how that works.I am so glad I never taken up C. This sound like a nightmare of a DX to me.
And these days, if you're bothering with C you probably care about these things. Accidentally promoting from the stack to the heap would be annoying.