相比于 c/c++,golang 的一个很大的改进就是引入了 gc 机制,再也不须要用户本身管理内存,大大减小了程序因为内存泄露而引入的 bug,可是同时 gc 也带来了额外的性能开销,有时甚至会由于使用不当,致使 gc 成为性能瓶颈,因此 golang 程序设计的时候,应特别注意对象的重用,以减小 gc 的压力。而 slice 和 string 是 golang 的基本类型,了解这些基本类型的内部机制,有助于咱们更好地重用这些对象c++
slice 和 string 的内部结构能够在 $GOROOT/src/reflect/value.go
里面找到git
type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int }
能够看到一个 string 包含一个数据指针和一个长度,长度是不可变的github
slice 包含一个数据指针、一个长度和一个容量,当容量不够时会从新申请新的内存,Data 指针将指向新的地址,原来的地址空间将被释放golang
从这些结构就能够看出,string 和 slice 的赋值,包括当作参数传递,和自定义的结构体同样,都仅仅是 Data 指针的浅拷贝数组
si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} si2 := si1 si2 = append(si2, 0) Convey("从新分配内存", func() { header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1)) header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2)) fmt.Println(header1.Data) fmt.Println(header2.Data) So(header1.Data, ShouldNotEqual, header2.Data) })
si1 和 si2 开始都指向同一个数组,当对 si2 执行 append 操做时,因为原来的 Cap 值不够了,须要从新申请新的空间,所以 Data 值发生了变化,在 $GOROOT/src/reflect/value.go
这个文件里面还有关于新的 cap 值的策略,在 grow
这个函数里面,当 cap 小于 1024 的时候,是成倍的增加,超过的时候,每次增加 25%,而这种内存增加不只仅数据拷贝(从旧的地址拷贝到新的地址)须要消耗额外的性能,旧地址内存的释放对 gc 也会形成额外的负担,因此若是可以知道数据的长度的状况下,尽可能使用 make([]int, len, cap)
预分配内存,不知道长度的状况下,能够考虑下面的内存重用的方法app
si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} si2 := si1[:7] Convey("不从新分配内存", func() { header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1)) header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2)) fmt.Println(header1.Data) fmt.Println(header2.Data) So(header1.Data, ShouldEqual, header2.Data) }) Convey("往切片里面 append 一个值", func() { si2 = append(si2, 10) Convey("改变了原 slice 的值", func() { header1 := (*reflect.SliceHeader)(unsafe.Pointer(&si1)) header2 := (*reflect.SliceHeader)(unsafe.Pointer(&si2)) fmt.Println(header1.Data) fmt.Println(header2.Data) So(header1.Data, ShouldEqual, header2.Data) So(si1[7], ShouldEqual, 10) }) })
si2 是 si1 的一个切片,从第一段代码能够看到切片并不从新分配内存,si2 和 si1 的 Data 指针指向同一片地址,而第二段代码能够看出,当咱们往 si2 里面 append 一个新的值的时候,咱们发现仍然没有内存分配,并且这个操做使得 si1 的值也发生了改变,由于二者本就是指向同一片 Data 区域,利用这个特性,咱们只须要让 si1 = si1[:0]
就能够不断地清空 si1 的内容,实现内存的复用了函数
PS: 你可使用 copy(si2, si1)
实现深拷贝性能
Convey("字符串常量", func() { str1 := "hello world" str2 := "hello world" Convey("地址相同", func() { header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) fmt.Println(header1.Data) fmt.Println(header2.Data) So(header1.Data, ShouldEqual, header2.Data) }) })
这个例子比较简单,字符串常量使用的是同一片地址区域测试
Convey("相同字符串的不一样子串", func() { str1 := "hello world"[:6] str2 := "hello world"[:5] Convey("地址相同", func() { header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) fmt.Println(header1.Data, str1) fmt.Println(header2.Data, str2) So(str1, ShouldNotEqual, str2) So(header1.Data, ShouldEqual, header2.Data) }) })
相同字符串的不一样子串,不会额外申请新的内存,可是要注意的是这里的相同字符串,指的是 str1.Data == str2.Data && str1.Len == str2.Len
,而不是 str1 == str2
,下面这个例子能够说明 str1 == str2
可是其 Data 并不相同ui
Convey("不一样字符串的相同子串", func() { str1 := "hello world"[:5] str2 := "hello golang"[:5] Convey("地址不一样", func() { header1 := (*reflect.StringHeader)(unsafe.Pointer(&str1)) header2 := (*reflect.StringHeader)(unsafe.Pointer(&str2)) fmt.Println(header1.Data, str1) fmt.Println(header2.Data, str2) So(str1, ShouldEqual, str2) So(header1.Data, ShouldNotEqual, header2.Data) }) })
实际上对于字符串,你只须要记住一点,字符串是不可变的,任何字符串的操做都不会申请额外的内存(对于仅内部数据指针而言),我曾自做聪明地设计了一个 cache 去存储字符串,以减小重复字符串所占用的空间,事实上,除非这个字符串自己就是由 []byte
建立而来,不然,这个字符串自己就是另外一个字符串的子串(好比经过 strings.Split
得到的字符串),原本就不会申请额外的空间,这么作简直就是画蛇添足
转载请注明出处
本文连接:http://hatlonely.com/2018/03/17/golang-slice-%E5%92%8C-string-%E9%87%8D%E7%94%A8/