golang中slice切片解析
在平常的撸码过程中,我们一般接触最多的是切片,所以呢,今天就讨论一下slice的用法,有说的不对的地方,大家一起讨论哦,悉听教诲,哈哈......😄
slice含义
slice是一个简化版的动态数组,长度不固定,相对于数组来说,slice使用更常见。
slice结构定义
可以使用reflect.SliceHeader查看Slice的结构:
type Slice struct {
Data uintptr
Len int
Cap int
}
这个可以发现,是和golang中的string的开头部分结构是一致的,顺便简单说下哈⬛️String结构:
type StringHeader struct{
Data uintptr
Len int
}
Slice多了一个Cap成员,这个表示切片指向的内存空间的最大容量(这个对应元素的个数,而不是元素的字节数)。下面简单画了一下切片的内存结构,画的不好看...但是要表达的意思还是达到了,哈哈:
slice的使用技巧
一般可以使用make进行初始化,也可以直接定义,这里只是简单说一下:
var (
a []int //nil切片,和nil是相等的,一般用来表示一个不存在的切片
b = []int{} //空切片,和nil不相等,表示一个空的集合
c := []int{1,2,3} //有3个元素的切片,len和cap都是3
d := c[:2] //有2个元素的切片,长度为2,但是容量是3
e := c[0:2:cap(c)] //有2个元素,长度为2,容量为3
f := c[:0] //有0个元素,长度为0,容量为3
//可以使用make指定长度和容量
g := make([]int,3) //容量和长度都为3
h := make([]int,2,3) //长度为2,容量为3
)
一般可以使用len()函数返回切片的真实长度,cap()返回切片容量的大小,容量必须大于或等于切片的长度。切片可以和nil进行比较,只有当切片底层数据指针为空的时候才是nil。
对于切片的添加删除等操作,可以使用append,copy等函数进行操作,这里就赘述了。提醒一下,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多的,当然,在容量不足的情况下,append()操作会导致重新分配内存,这样,就会导致巨大的内存分配和复制数据,这里,感兴趣的话,可以看下go源码的slice解析,对于slice的扩容机制做了解析。
切片的高效操作:
<u>在使用切片的过程中,一定要降低内存分配的次数,尽量保证append()操作不会超过cap的容量,可以降低内存分配的次数和每次分配内存的大小。</u>
切片的注意事项:
切片的操作并不会复制底层的数据,底层的数组会被保存在内存中,直到不再被引用,但是有时候可能会因为一个小的内存引用导致底层整个数组处于被使用状态,这样会延迟垃圾回收器对底层数组的回收。
这里,举一个简单的例子:
比如这个函数:
func findMatchString( filename string)[]byte{
bytes,_ := ioutil.ReadFile(filename)
return regexp.MustCompile("[0-9]+").Find(bytes)
}
这段代码返回的是[]byte是指向保存的整个文件的数组,这里呢,由于切片使用了整个原始数组,导致垃圾回收器不能及时释放底层的数组的空间,可能需要长时间保存整个文件数据,这个会降低系统的整体性能。一般做法呢,可以将需要的数据复制到一个新的切片中,切断对原始数据的依赖。
同样,针对切片存放的是指针对象,那么删除末尾的元素,被删除的元素依然被切片底层数组引用,从而导致不能及时被垃圾回收器。比如下面的实现方式:
var a []*int{...}
a = a[:len(a)-1] //被删除的最后一个元素还是被引用,可能导致垃圾回收器操作被阻碍。较为保险的方式使用:
a[len(a)-1]= nil //垃圾回收器回收最后一个元素内存
a = a[:len(a)-1] //从切片删除最后一个元素
当然,如果切片存在的周期很短的话,这个就不需要了。。。
最后,slice大致就说到这里了,大家对slice的底层实现感兴趣的话,可以参考源码解析,本文主要针对slice的使用上做了一定的介绍,就当是抛砖引玉。。。