文章目录
- ● Slice 的底层实现原理?
- ● array 和 Slice 的区别?
- ● 拷贝大切片一定比小切片代价大吗?
- ● Slice 深拷贝和浅拷贝?
- ● 零切片、空切片、nil切片?
- ● Slice 的扩容机制?
- ● Slice 为什么不是线程安全的?如何解决这个问题?
- ● 参数传递切片和切片指针有什么区别?
- ● range 遍历切片的时候有什么要注意的?
● Slice 的底层实现原理?
切片是对底层数组的引用,在创建一个切片的时候,Go会自动分配一个底层数组,并维护这个数组的指针、切片的长度和容量。
● array 和 Slice 的区别?
1、数组是固定长度的,不能动态扩容,在编译期就会确定大小,切片是可以自动扩容的。 2、数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变。
● 拷贝大切片一定比小切片代价大吗?
不一定。切片中的第一个字段是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个切片变量分配给另一个变量只会复制三个机器字,大切片跟小切片的区别无非就是 Len 和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。
● Slice 深拷贝和浅拷贝?
浅拷贝是创建一个新的切片,但这个新切片仍然引用原切片的底层数组。 深拷贝是创建一个新的切片,并复制原来切片的所有元素到新的切片中。
● 零切片、空切片、nil切片?
零切片
我们把切片内部数组的元素都是零值或者底层数组的内容就全是 nil的切片叫做零切片,使用make创建的、长度、容量都不为0的切片就是零值切片:slice := make([]int,5) // 0 0 0 0 0 slice := make([]*int,5) // nil nil nil nil nil
nil切片
nil切片的长度和容量都为0,并且和nil比较的结果为true,采用直接创建切片的方式或new创建切片的方式:var slice []int var slice = *new([]int)
空切片
空切片的长度和容量也都为0,但是和nil的比较结果为false,因为所有的空切片的数据指针都指向同一个地址 0xc42003bda0;使用字面量、make可以创建空切片:var slice = []int{} var slice = make([]int, 0) 05. 切片的扩容策略
● Slice 的扩容机制?
新申请的容量如果大于当前容量的两倍,会将新申请的容量直接作为新的容量,如果新申请的容量小于当前容量的两倍,会有一个阈值,即当前切片容量小于256时,切片会将当前容量的2倍作为新申请的容量,如果大于等于256,每次容量会增加 (旧容量+3*256)/4。
● Slice 为什么不是线程安全的?如何解决这个问题?
切片的底层数据结构体包括一个指向数组的指针,在多个线程同时对 切片进行操作时会导致竞态条件。(并发读写问题、切片扩容问题) 通过使用适当的同步机制如互斥锁、读写锁,确保对切片的并发访问是安全的,从而避免竞态条件和数据破坏。 (竞态条件是指在并发程序中,由于多个线程或goroutine同时访问或修改共享资源,从而导致程序的执行结果依赖于线程或goroutine的调度顺序,可能产生不确定和错误的结果。)
● 参数传递切片和切片指针有什么区别?
当切片作为参数传递时,其实就是一个结构体的传递,因为Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为底层数据的地址没有变,所以在函数内对切片的修改,也将会影响到函数外的切片。
在 Go 语言中,切片虽然是引用类型,但在函数中使用 append 操作时,需要注意一些细节。append 函数可能会导致切片底层数组重新分配内存,从而影响切片的引用传递。
具体来说,当 append 导致切片容量不足而需要扩容时,append 操作会创建一个新的底层数组,并返回新的切片引用。
由于你没有将这个新引用返回给原切片,原切片仍然指向旧的底层数组,因此不会体现出引用传递的效果。
参数传递切片指针就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底层数组,则应该按指针传递。
● range 遍历切片的时候有什么要注意的?
副本与原始切片: 在range遍历中,实际上会创建每个元素的副本。这意味着你在循环中对副本的修改不会影响原始切片。如果需要修改原始切片,应该使用索引来操作。
只读: 默认情况下,range遍历是只读的,不能修改切片的元素。如果尝试在range循环中修改元素值,会引发编译错误。
索引和值的顺序: 在range循环中,索引总是在前,元素值总是在后。如果只需要索引而不需要元素值,可以使用下划线 _ 来忽略元素值。
切片遍历的性能考虑:尽管使用range遍历切片是非常方便的,但在性能方面可能会有一些影响。每次使用range遍历时,都会创建元素的副本,这可能导致额外的内存开销和性能下降。如果在性能敏感的场景下遇到性能问题,你可以考虑使用索引遍历,以避免创建副本。另外,如果需要在遍历时修改切片的元素,也应该使用索引遍历,以确保修改能够直接影响到原始切片。
切片为空: 如果切片为空,使用range遍历不会执行任何循环体,这可能会导致你的代码出现逻辑问题。在使用range之前,最好先检查切片的长度。
切片为nil: 如果切片为nil,使用range遍历会引发运行时错误。同样,遍历之前应该确保切片不为nil。
遍历数组 vs. 切片: 在数组上使用range遍历和在切片上使用range遍历是不同的。对于数组,range返回索引和元素的副本,而在切片上,range返回索引和元素值的副本。