./a.go:5:6: cannot inline main: function too complex: cost 104 exceeds budget 80
./a.go:12:20: inlining call to fmt.Println
./a.go:12:21: num escapes to heap:
./a.go:12:21: flow: {storage for ... argument} = &{storage for num}:
./a.go:12:21: from num (spill) at ./a.go:12:21
./a.go:12:21: from ... argument (slice-literal-element) at ./a.go:12:20
./a.go:12:21: flow: fmt.a = &{storage for ... argument}:
./a.go:12:21: from ... argument (spill) at ./a.go:12:20
./a.go:12:21: from fmt.a := ... argument (assign-pair) at ./a.go:12:20
./a.go:12:21: flow: {heap} = *fmt.a:
./a.go:12:21: from fmt.Fprintln(os.Stdout, fmt.a...) (call parameter) at ./a.go:12:20
./a.go:7:19: make([]int, 10) does not escape
./a.go:12:20: ... argument does not escape
./a.go:12:21: num escapes to heap
复制代码
哪些东西逃逸了哪些没有显示得一清二楚——escapes to heap表现变量或表达式逃逸了,does not escape则表现没有发生逃逸。
别的本文讨论的是go官方的gc编译器,像一些第三方编译器比如tinygo没义务也没来由使用和官方完全相同的逃逸规则——这些规则并不是标准的一部分也不适用于某些特殊场景。
本文的go版本是1.23,我也不盼望未来某一天有人用1.1x或者1.3x版本的编译器来问我为啥实验结果不一样了。
八股文里的问题
./b.go:23:21: flow: {storage for ... argument} = &{storage for str}:
./b.go:23:21: from str (spill) at ./b.go:23:21
./b.go:23:21: from ... argument (slice-literal-element) at ./b.go:23:20
./b.go:23:21: flow: fmt.a = &{storage for ... argument}:
./b.go:23:21: from ... argument (spill) at ./b.go:23:20
./b.go:23:21: from fmt.a := ... argument (assign-pair) at ./b.go:23:20
./b.go:23:21: flow: {heap} = *fmt.a:
./b.go:23:21: from fmt.Fprintln(os.Stdout, fmt.a...) (call parameter) at ./b.go:23:20
./b.go:21:14: &S{} does not escape
./b.go:23:20: ... argument does not escape
./b.go:23:21: str escapes to heap
复制代码
我只截取了关键信息,否则杂音太大。&S{} does not escape这句直接告诉我们getString的参数并没有逃逸。
为啥?因为getString被内联了,内联后编译器发现参数的实际类型就是S,所以devirtualizing s.String to *S做了去假造化,这下接口的实际类型编译器知道了,所以没有让参数逃逸的必要了。
而str逃逸了,str的类型是已知的,内容也是常量字符串,按八股文的理论不是不应该逃逸么?其实上面的信息也告诉你为什么了,因为fmt.Println内部的一些函数没法内联,而它们又用any去担当参数,这时间编译器没法做去假造化,没法最终确定变量的真实巨细,所以str只能逃逸了。记得最开头我说的吗,逃逸分析是很保守的,因为内存安全和步调的正确性是第一位的。
如果禁止函数inline,环境就不同了,我们在go里可以手动禁止一个函数被内联: