Go语言学习 - 复合数据类型:数组、slice
复合数据类型
主要分为4种复合数据类型:
- 数组
- slice
- map
- 结构体
本次先将数组和slice的相关知识点及理解记录下来,后续再慢慢看Map和结构体。
数组
数组是具有固定长度且拥有零个或多个相同数据类型元素的序列。由于数组的长度固定,所以在Go里面很少直接使用。
var arr [3]int // 3个整数的数组
fmt.Println(arr[len(arr)-1]) // 输出数组最后一个元素
// 遍历数组并输出索引和元素
for i, v := range arr {
fmt.Println("%d %d\n", i, v)
}
- 数组支持在定义时初始化值
var arr [3]int = [3]{1, 2, 3}
- 数组支持根据定义时设置的初始化值自动推导数组长度
arr := [...]int{1, 2, 3}
数组的长度是数组类型的一部分,数组的长度不同,类型不相等。
数组支持在定义时使用 {索引:值} 的形式定义,例如可以为指定索引赋值。如果在使用索引初始化时,数组的长度不按元素个数计 算,而且根据最大索引值计算,例如:
// 定义一个长度100的数组,第99个元素(最后一个)的值为 2
r := [...]int{99: 2}
- 如果一个数组的元素类型是可比的(元素类型一致),那么该数组 也是可比的。数组可以使用 == 或者 != 进行比较,例如:
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [3]int{1, 2, 3}
fmt.Println( a == b ) // true
fmt.Println( a == c ) // 编译错误:数组类型不一致
当调用一个函数时,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量,所以函数接受的是一个副本,而不是原始的参数。使用这种方式传递大的数组会变得很低效,并且在函数内部对数组的任何修改都仅影响副本,而不是原始数组。这种情况下,Go把数组和其他的类型都看成了值传递。在其他语言中,例如Java数组是隐式地使用引用传递。需要传递数组引用可以使用指针方式
slice
理解:在看到slice前面部分时,自动带入了Java中List的概念去看,导致反复看也不是很清楚。最终看到make函数、append函数 让我理解了,的确可以理解为List来使用。
slice:切片,是对数组一段内容的引用。切片的定义与数组基本一致,但不需要在[]中填写数组的长度。
// 数组定义
a := [...]int{1, 2, 3, 4, 5}
// 切片定义
s := []int{1, 2, 3, 4, 5}
slice有3个属性:指针、长度和容量。
- 指针指向数组的第一个可以从slice中访问的元素(并不一定是数组的第一个元素,具体看切片如何“切” 的数组)
- 长度是指slice中的元素个数,它不能超过slice的容量
- 容量的大小通常是从slice的起始元素到底层数组(被切数组)的最后一个元素间的元素个数
可以使用内置的len() 和 cap() 函数来计算切片的长度和容量。
如果slice的引用超过了被引用对象的容量,即cap(被引用对象),那么会导致程序报错。 如果slice的引用超过了被引用都对象的长度,即len(被引用对象),那么最终slice会比原slice长。
// 月份
months := [...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "Novemeber", 12: "Decemeber"}
// 实际夏季的月份
summer := months[6:9]
// 超出cap(summer) ,报错
fmt.Println(summer[:20])
// 扩充长度
summer1 := summer[:5] //summer1 = [June July August September October]
在看了上面最后一句代码,可以更清晰认为slice只是引用了数组,所有变化也是数组,所以slice可以理解为数组的一个别名(一个区域的别名)
比较
slice和数组基本一致,除初始化方式不需要填写长度之外,slice也不可以直接用 == 或者 !=比较。如果是[]bytes的slice,可以使用bytes.Equal 进行比较,其余类型需要自己写函数实现。
slice不支持直接使用 == 或者 != 比较的原因有以下2点:
- 和数组不同,slice的元素是非直接的,有可能slice可以包含它本身
- 因为slice的元素不是直接的,所以如果底层数组元素改变,同一个slice在不同的时间会拥有不同的元素。
make 函数
内置的make函数可以创建一个具有指定元素类型、长度和容量的slice。如果省略容量参数,则容量 == 长度。
在make函数的内部实现中,make函数创建了一个无名的数组并返回了它的一个slice,这个数组进可以通过这个slice访问。
// slice引用的无名数组长度 == 传入的len
make([]T, len)
// slice引用的无名数组长度 == cap, 并返回指定len的slice
make([]T, len, cap)
append 函数
内置的append函数用于将元素追加到slice的后面。append函数可以同时给slice添加多个元素,甚至添加另一个slice里的所有元素。
通过slice.go源码中的growslice函数查看slice的扩充策略:当旧的容量小于1024,则新容量直接*2 (doublecap := newcap + newcap ),反之,每次增加1/4。
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
在slice每次扩充时,会返回一个新的slice,而旧的slice有可能指向了底层数组,也有可能没有指向底层数组,如果slice的容量完全能够存在新的内容,则指向了旧数组,否则返回的新slice与原slice不一致(底层数组不一致)。所以为了保证在后面能够继续使用变量操作,可以将append的结果赋值给原slice
runes = append(runes, r)
// 思考以下代码的输出
func main() {
s := []int{3}
s = append(s, 4)
s = append(s, 5)
x := append(s, 6)
y := append(s, 7)
fmt.Println(s, x, y)
}
// @TODO 后面学完Go基础后,将slice.go源码仔细阅读一遍。