go 数据结构 -- array&slice

2021-03-24  本文已影响0人  朱建涛

数据类型的本质:固定内存大小的别名。

数据类型的作用:编译器预算对象或变量分配内存空间的大小。

数组 array

数组是同一种数据类型的固定长度的序列,指向一段连续的内存空间。

声明

arr := [6]int{1,2,3,4,5,6}

特性

数组指针和指针数组

数组指针

即声明一个指针变量,指向一个数组:

arr := [6]int{5:9}  //第6个元素为9
var ptr *[6]int = &arr

需要注意的是,指针变量ptr的类型是*[6]int,也就是说它只能指向包含6个元素的整型数组,否则编译报错。

指针数组

即数组的元素类型是指针。

x,y := 1,2
var arrPtr = [5]*int{1:&x,3:&y}

函数间传递数组

如果实参是一个非常大的数组,因函数参数只有值传递,即需要重新拷贝变量,拷贝导致的内存和性能开销较大。可将数组指针作为参数传递,拷贝开销小,但也需要考虑到函数内数组指针修改会影响原数组。

切片 slice

切片与数组类似,存放相同数据类型的元素,不同的是切片基于底层数组,可按需扩缩容。切片是底层数组的一个视图,也可以说切片是对数组的抽象。

切片的内部实现中有三个变量,指针 ptr,长度 len 和容量 cap。

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 指针,数据存储在底层数组中,而指针指向可以通过切片访问到的第一个元素。
    len   int            // 长度,我们只能访问切片长度范围内的元素。
    cap   int            // 容量,表示可以扩展的最大大小。容量必须大于等于长度。
}
图片

应注意底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

声明与初始化

make 函数创建

s1 := make([]int,3,5)   // 创建一个可用的空切片,长度为3,容量为5,容量也可以不传
fmt.Println(s1)         // [0 0 0]

字面量创建

s2 := []int{1,2,3,4,5}  // 创建长度和容量都是5的整型切片

截取已有的数组或者切片创建

a := []int{1,2,3,4,5}
t := a[3:4]

截取得到的切片,和原切片或原数组共享底层数组,但是两者能访问到底层数组的范围是不同的。

截取获得的新切片的长度和容量计算

对容量为 k 的切片执行[i,j]操作之后,获得的新切片的长度和容量是j-ik-i

对容量为 k 的切片执行[i,j,l]操作之后,获得的新切片的长度和容量是j-il-i,其中第三个变量 l 用于限定新切片的容量,j 和 l 必须在原数组或者原 slice 的容量范围内(小于等于 k)。

新切片与原数组或者切片的关系

截取得到的新切片和原数组或原切片是基于同一个底层数组的,所以当修改的时候,底层数组的值就会被改变,原切片的值也随之改变了。

可以把截取得到的新切片看做是原数组或原切片的一个视图。

图片

但如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组

nil 切片和空切片

nil 切片

var s []int                 // 声明了一个 nil 切片
fmt.Println(s == nil)       // 输出 true
fmt.Println(len(s), cap(s)) // 输出:0 0
s = append(s, 1)            // 使用 append 函数可以为 nil 切片增加元素

切片的零值是 nil。因为切片是底层数组的引用,nil 切片指向底层数组的指针为 nil,即不指向任何底层数组。

空切片

s := make([]int, 0)         // 1、使用 make 创建空的整型切片
s := []int{}                // 2、使用切片字面量创建空的整型切片
fmt.Println(s == nil)       // 输出 false
fmt.Println(s)              // 输出:[]
fmt.Println(len(s), cap(s)) // 输出:0 0

与 nil 切片一样,空切片的长度和容量也都是 0,说明切片底层的数组大小为 0,是一个空数组(没有分配任何的存储空间)。

不管是使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的效果都是一样的。官方建议尽量使用 nil 切片。

nil slice 可以直接 append ,因为 append 最终都是调用 mallocgc 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的 nil slice 或 empty slice。

增长/扩容

使用内建函数 append 能够帮我们处理切片增长的一些细节。

要注意的是,通过截取创建的切片,如果切片剩余容量能存下 append 追加的元素,切片长度增长而不扩容,追加的元素会改变原切片或数组的值;如果不能存下追加的元素,切片长度增长并进行扩容,扩容操作为 append 函数创建一个新的底层数组,将原数组的值复制到新数组里,再追加新的值,因为新切片与原切片或数组的底层数组不再相同,追加的元素不会改变原切片或数组的值,且后续两个切片不再相互影响。关键在于两者是否会共用底层数组

一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题。

copy 函数

Go 提供了内置函数 copy,可以将一个切片复制到另一个切片。

func copy(dst, src []Type) int  // 函数返回两者长度的最小值

如果 dst 切片为 nil 切片,copy 之后,dst 切片仍为 nil。而 nil 切片 append 非空切片后,变为非空切片。

copy 只会复制,不会追加。

函数间传递切片

切片在函数间以值的方式传递。当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。

由于切片的尺寸很小(在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段、长度和容量字段各需要 8 字节),在函数间复制和传递切片成本很低。

删除切片中的元素

Go 没有提供删除切片元素的函数,可以通过截取和 append 函数实现。

s := []int{1, 2, 3, 4, 5, 6}
s = append(s[:2], s[3:]...)    // 删除索引为2的元素

其他骚操作

切片垃圾回收

巨型 slice 产生的垃圾回收问题

上一篇下一篇

猜你喜欢

热点阅读