Хотя есть несколько отличных статей о # escape-analysis для #go, этот пост - моя попытка демистифицировать и представить тему как можно проще.

Я буду запускать несколько сценариев и раскрывать информацию анализа выхода компилятора, используя следующую команду:

go build -gcflags “-m -l”

Эти сообщения состоят из двух частей:

  1. Часть 1: вы уже на этой странице
  2. Часть 2: ‹link›

Каждый сценарий отформатирован следующим образом:

// ** Scenario #** //
// change: <brief text to describe the scenario>
// program that I ran
...
// escape analysis output
...
// explanation
...
// ** Scenario 1** //
// not using any pointers
// program that I ran
package del
type S struct {
 x int
}
func f1() {
   x:= S{1}
   _ = f2(x)
}
func f2(x S) S {
 y := x
   return y
}
// escape analysis output
<blank>
// explanation
Since there are no pointers/references all variables are local to the function stack. No heap allocation required
// ** Scenario 2 ** //
// change: returning a pointer to a struct created in the called function
// program that I ran
package del
type S struct {
   x int
}
func f1() {
   x:= S{1}
   _ = f2(x)
}
func f2(x S) *S {
   y := x
   return &y
}
/*
// escape analysis output
# del
.\del.go:13:2: moved to heap: y
// explanation
In scenario 2, “y” was moved to heap by the compiler because f2 is returning the reference to y. And upon return stack of f2 will be marked invalid. Thus for f1 to use the reference to y, y must be available even after f2 has finished.
// ** Scenario 3 ** //
// change: returning a struct created in the called function using a pointer to a struct in the calling function
// program that I ran
package del
type S struct {
   x int
}
func f1() {
   s:= S{1}
   _ = f2(&s)
}
func f2(x *S) S {   
   y := *x
   return y
}
/*
// escape analysis output
# del
.\del.go:12:9: f2 x does not escape
// explanation
Here, x does not escape and stays on f1's stack. This is because while f2 is running f1's stack will still be available
*/
// ** Scenario 4** //
// change: returning a pointer to the struct created in the called function using a pointer to a struct in the calling function
// program that I ran
package del
type S struct {
   x int
}
func f1() {
   s:= S{1}
   _ = *f2(&s)
   _ = *f3(&s)
}
func f2(x *S) *S {   
   y := x
   return y
}
func f3(x *S) *S {   
   y := *x // line 18
   return &y
}
/*
// escape analysis output
# del
.\del.go:13:9: leaking param: x to result ~r1 level=0
.\del.go:17:9: f3 x does not escape
.\del.go:18:4: moved to heap: y
// explanation
We have 2 functions f2 and f3, that achieve the same thing i.e. accept a *S and return a *S. In f2, y and x are both references to the stack address in f1. While in f3, y contains the value of x and lives on f3's stack. Now since f1 dereferences what f3 returns, y needs to be available on the heap for f1 to deference.
*/
// ** Scenario 5 ** //
// change: constructing the struct in the called functions
// program that I ran
package del
type S struct {
   x int
}
func f1() {
   i := 123
   _ = *f2(i)  
   _ = f3(&i)
   _ = *f4(&i)
}
func f2(x int) *S {   
   y := S{x} // line 16
   return &y
}
func f3(x *int) S {   
   y := S{*x}
   return y
}
func f4(x *int) *S {   
   y := S{*x} // line 24
   return &y
}
/*
// escape analysis output
# del
.\del.go:16:4: moved to heap: y
.\del.go:19:9: f3 x does not escape
.\del.go:23:9: f4 x does not escape
.\del.go:24:4: moved to heap: y
// explanation
Shouldn't be a surprise for why y in f2 and f4 is moved to heap
*/
// ** Scenario 6 ** //
// change: S.x is not *int type
// program that I ran
package del
type S struct {
   x *int
}
func f1() {
   i := 123
   _ = *f2(i)  
   _ = f3(&i)
   _ = *f4(&i)
}
func f2(x int) *S {   
   y := S{&x} // line 16
   return &y
}
func f3(x *int) S {   
   y := S{x} // line 20 
   return y
}
func f4(x *int) *S {   
   y := S{x} // line 24
   return &y
}
/*
// escape analysis output
# del
.\del.go:15:9: moved to heap: x
.\del.go:16:4: moved to heap: y
.\del.go:19:9: leaking param: x to result ~r1 level=0
.\del.go:23:9: leaking param: x
.\del.go:24:4: moved to heap: y
.\del.go:10:4: moved to heap: i
// explanation
For f2, it should be clear why y is being moved to heap. x is also moved to heap for y to continue referencing it even after f2 returns
For f3, y is being returned to f1 where x already exists. So no need to place anything on heap
For f4, it should be cleat why y is being moved to heap. i (=x since same reference) is also moved to heap for y to continue referencing it even after f1 returns
*/
// ** Scenario 7 ** //
// change: function assigns value to a member of the struct, and returns nothing
// program that I ran
package del
type S struct {
   x *int
}
func f1() {
   i := 123
   var s S
   f2(i, s)
   f3(&i, s)
   f4(i, &s)
   f5(&i, &s)
}
func f2(x int, y S) {   
   y.x = &x // line 18
}
func f3(x *int, y S) {   
   y.x = x // line 21
}
func f4(x int, y *S) {   
   y.x = &x // line 22 x
}
func f5(x *int, y *S) {   
   y.x = x // line 26 x
}
/*
// escape analysis output
# del
.\del.go:17:16: f2 y does not escape
.\del.go:20:9: f3 x does not escape
.\del.go:20:17: f3 y does not escape
.\del.go:23:9: moved to heap: x
.\del.go:23:16: f4 y does not escape
.\del.go:26:9: leaking param: x
.\del.go:26:17: f5 y does not escape
.\del.go:10:4: moved to heap: i
// explanation
For f2, nothing is moved to heap since everything lives on the stack of f2 and isn't required once f2 returns
For f3, nothing is moved to heap since everything lives on the stack of f3 and isn't required once f3 returns
For f4, y is coming from another stack and for x to be available after f4 returns, x must be moved to the heap
For f5, same explanation as f4, x must be moved to heap except that x here refers to i since both are pointers to same address. Hence, i must be moved to heap
*/

Далее: Часть 2 будет посвящена работе со срезами и картами.

Домашнее задание для читателей:

В. Что такое карты на самом деле?

A. Проверьте это: https://golang.org/src/runtime/map.go#L114

В. Что такое срезы?

A. Проверьте это: https://golang.org/pkg/reflect/#SliceHeader