容器类型
java
java中的容器类型常用的是List,Set,HashMap等。
在java中谈容器,一般指的是Collection和Map。数组不属于容器的范围。
但是go中我们说到容器类型,一般是说数组、切片和map
go的数组
go数组的两个特性:长度固定,元素类型相同。
var arrName [n]T //长度为n,类型是T
[n]T 是arrName的数组类型。也就是说如果两个数组类型的元素类型 T 与数组长度 N 都是一样 的,那么这两个数组类型是等价的,如果有一个属性不同,它们就是两个不同的数组类 型。
因为数组在定义时其类型和长度都是明确的,所以实际内存分配上,也是一块连续的,可容纳所有数据的内存。
// 数组的声明
var a [3]int // 默认初始化为int的零值
a[0] = 1
b := [3]int{1, 3, 5} // 声明同时初始化
c := [2][2]int{{1, 1}, {2, 2}} //多维数组
d := [...]int{1, 2, 3, 4, 5} // 不用写数组的长度
var e = [...]int{ // 稀疏数组
99: 39, // 将第100个元素(下标值为99)的值赋值为39,其余元素值均为0
}
Go 提供了预定义函数 len 可以用于获取一 个数组类型变量的长度,通过 unsafe 包提供的 Sizeof 函数,我们可以获得一个数组变量 的总大小
t.Log(len(d)) // 5
t.Log(unsafe.Sizeof(d)) // 40
切片slice
切片的定义和数组很像,仅仅是少了一个“长度”属性。切片的存在是为了解决数组的问题,数组长度固定,很不灵活。
var myslice = []int{1, 2, 3, 4, 5, 6}
使用内置的append函数添加元素
myslice = append(myslice, 100)
slice的实现
slice的底层结构是
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片的长度,即切片中当前元素的个数;
cap int // 底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值
}
每个新建的slice都会新建一个底层数组。数组的长度和切点初始元素的个数相同。
我们还有其他方法创建切片。
- 通过make 函数来创建切片,并指定底层数组的长度 c := make([]int, 3, 5) // 切点的len是3,cap是5,即底层数组的长度是5.如果不指定。默认cap = len
t.Log(len(c), cap(c))
- 在已有数组的基础上创建切片
- 采用 array[low : high : max]语法基于一个已存在的数组创建切片。这种方式被 称为数组的切片化 func TestArr2Slice(t *testing.T) {
month := [12]string{“Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”, “Oct”, “Nov”, “Dec”}
slc := month[3:6:9]
t.Log(slc) // [Apr May Jun]
t.Log(len(slc), cap(slc)) // 3 6
} len = high – lowcap = max – low。通常省略max,max默认是数组的长度注意1,现在这个切片slc是直接指向数组month的。也就是说slc的改变会直接改变数组month slc[0] = “4月”
t.Log(month) // [Jan Feb Mar 4月 May Jun Jul Aug Sep Oct Nov Dec] 注意2,对一个数组可以创建多个切片。因为这些切片底层都是指向数组的。所以任意一个切片的改变都会影响其他切片。 slc2 := month[3:6]
t.Log(slc2) // [4月 May Jun]
t.Log(len(slc2), cap(slc2)) // 3 9
slc2[1] = “5月”
t.Log(month) // [Jan Feb Mar 4月 5月 Jun Jul Aug Sep Oct Nov Dec]
t.Log(slc) // [4月 5月 Jun] - 基于切片创建切片用法和基于数组创建切片一样,底层指向同一个数组,所以互相影响。
slice的动态扩容
slice相比array的特点就是不定长。当len == cap时,再对slice进行append,就会发生切片的动态扩容。
// 切片的容量是翻倍增加
func TestSliceGrowing(t *testing.T) {
s := []int{}
for i := 0; i < 20; i++ {
s = append(s, i)
t.Log(len(s), cap(s))
}
}
结果打印:
slice_test.go:49: 1 1
slice_test.go:49: 2 2
slice_test.go:49: 3 4
slice_test.go:49: 4 4
slice_test.go:49: 5 8
slice_test.go:49: 6 8
slice_test.go:49: 7 8
slice_test.go:49: 8 8
slice_test.go:49: 9 16
slice_test.go:49: 10 16
slice_test.go:49: 11 16
slice_test.go:49: 12 16
slice_test.go:49: 13 16
slice_test.go:49: 14 16
slice_test.go:49: 15 16
slice_test.go:49: 16 16
slice_test.go:49: 17 32
slice_test.go:49: 18 32
slice_test.go:49: 19 32
slice_test.go:49: 20 32
可以看到slice的容量cap时翻倍增加的。
动态扩容导致的与原数组的分割
前面说了,切片可以从数组创建。切片的修改会直接修改原数组。但是,切片是可以继续追加元素的,那么切片追加元素超出了原数组的最大边界会怎么样呢?
func TestArr2SliceOut(t *testing.T) {
month := [12]string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
slc := month[3:6:9]
t.Log(slc) // [Apr May Jun]
t.Log(len(slc), cap(slc)) // 3 6
slc = append(slc, "7月")
t.Log(slc) // [Apr May Jun 7月]
t.Log(month) // [Jan Feb Mar Apr May Jun 7月 Aug Sep Oct Nov Dec]
slc = append(slc, "8月")
slc = append(slc, "9月")
slc = append(slc, "10月")
t.Log(slc) // [Apr May Jun 7月 8月 9月 10月]
t.Log(month)// [Jan Feb Mar Apr May Jun 7月 8月 9月 Oct Nov Dec]
}
还是用月份举例。slc切片出了4,5,6三个月。len是3,cap是6。
追加一个7月。此时还在slc的容量范围内,所以直接影响了原数组。
但是当追加到10月时,已经超出了slc的容量范围。此时会进行扩容。slc的扩容会创建一个新的数组,与原数组不在有关系。所以10月不会影响原数组。之后slc做的任何操作都与原数组无关。
同样的道理推广到多个切片指向同一个数组。当某一个切片发生扩容后,他便于其他切片不在指向同一数组,也就不会再相互影响了。
切片的扩容这里是经常埋坑的地方。
一定要清楚的认识到slice与底层数组的关系