引子
近日在网上看到有个go代码,比较有意思,说在第14行注释前和注释后,最后的打印结果是不同的:
package main
import (
"fmt"
)
func main(){
s := []byte("")
s1 := append(s, 'a')
s2 := append(s, 'b')
// 如果释放此行,打印的结果是 a b,否则打印的结果是b b
//fmt.Println(s1, "===", s2)
fmt.Println(string(s1), string(s2))
}
验证一下
我用go编译跑了一下,无论是否注释,都是输出”a b”, 而不是文中提到的”b b”.
那么是咋回事呢?我看了下我的go版本是:
go version go1.12.7 linux/amd64
而文中是go1.9. 幸好我还有go1.10的版本:
go version go1.10.4 linux/amd64
看下它的表现,果然结果是不同的,那就可以用go1.10和go1.12做个比较,看下是哪里的不同。
按照文中所述,输出”a b”和”b b”原因是在栈上分配和堆上分配和策略不同所致。
如果14行被注释,那么s将被分配在栈上,而由于是[]byte("")
,是将空字符串转成byte slice,在内部执行的是runtime.stringtoslicebyte, 默认分配了32B size的cap;而释放那行,s将逃逸至堆,从堆上分配时,slice的cap为0。由于初始cap不同,造成了结果不同。
至于slice和append的实现,文中说的很明白,这里不赘述了。
咋回事
我们先来看下旧版的go,为啥注释前后输出不同呢?
为了更好地说明,我们为其加些调试打印,一窥究竟:
package main
import (
"fmt"
)
func main(){
s := []byte("")
println(s) // 调试
s1 := append(s, 'a')
println(s1) // 调试
s2 := append(s, 'b')
println(s2) // 调试
// 如果释放此行,打印的结果是 a b,否则打印的结果是b b
//fmt.Println(s1, "===", s2)
fmt.Println(string(s1), string(s2))
}
go 1.10
注释掉
逃逸分析
[legacy@localhost tmp]$ go build -gcflags '-m' # _/home/legacy/tmp ./main.go:18:23: string(s1) escapes to heap ./main.go:18:23: string(s1) escapes to heap ./main.go:18:35: string(s2) escapes to heap ./main.go:18:35: string(s2) escapes to heap ./main.go:8:16: main ([]byte)("") does not escape ./main.go:18:16: main ... argument does not escape
输出
[0/32]0xc420045f00 [1/32]0xc420045f00 [1/32]0xc420045f00 b b
可以看到,注释那行后,[]byte("")
没有逃逸,并且一开始就有了32的cap。之后的append因为有空间,所以仍旧在原slice上操作,b会把a覆盖掉,所以输出b b
.
释放掉
逃逸分析
[legacy@localhost tmp]$ go build -gcflags '-m' # _/home/legacy/tmp ./main.go:17:16: s1 escapes to heap ./main.go:8:16: ([]byte)("") escapes to heap ./main.go:17:21: "===" escapes to heap ./main.go:17:21: s2 escapes to heap ./main.go:18:23: string(s1) escapes to heap ./main.go:18:23: string(s1) escapes to heap ./main.go:18:35: string(s2) escapes to heap ./main.go:18:35: string(s2) escapes to heap ./main.go:17:16: main ... argument does not escape ./main.go:18:16: main ... argument does not escape
输出
[0/0]0x543f18 [1/8]0xc420014068 [1/8]0xc420014080 [97] === [98] a b
释放注释后,[]byte("")
逃逸至堆上,并且分配的cap为0的sclie,之后的append均会重新生成一个slice,可以看到s1和s2所代表的slice内部的data地址是不同的,所以打印时,各取各的。
我们再来看为什么高版本的go1.12,注释以后,还是输出a b
。
go 1.12
注释掉
逃逸分析
[vagrant@localhost 3]$ go build -gcflags '-m' # escape_analyze/3 ./main.go:18:16: inlining call to fmt.Println /tmp/go-build106864236/b001/_gomod_.go:6:6: can inline init.0 ./main.go:18:23: string(s1) escapes to heap ./main.go:18:23: string(s1) escapes to heap ./main.go:18:35: string(s2) escapes to heap ./main.go:18:35: string(s2) escapes to heap ./main.go:18:16: io.Writer(os.Stdout) escapes to heap ./main.go:8:16: main ([]byte)("") does not escape ./main.go:18:16: main []interface {} literal does not escape <autogenerated>:1: os.(*File).close .this does not escape
输出
[0/0]0xc000034728 [1/8]0xc0000140a8 [1/8]0xc0000140c0 a b
从逃逸分析上看,[]byte("")
是在栈上,但是分配时,不像低版本golang,高版本分配了cap为0的slice,之后的append会创建新的slice,所以输出是a b
.
后记
不同golang版本的runtime.stringtoslicebyte的实现是不同的,可以查看源码得到。
实际上s:=[]byte("")
这样的写法不是很好,应该是
s:=[]byte{}
// or
var s []byte
更多的是append
的用法不合理,在同一个slice引用上append多次,结果取决于该slice的cap有多大。
一般写程序会避免这种写法,当然作为研究slice和逃逸分析的例子,还是不错的。