聊聊 Go 语言中的数组与切片

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战程序员

1. 数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组能够由零个或多个元素组成。由于数组的长度是固定的,所以在 Go 语言中不多直接使用数组。和数组对应的类型是 Slice(切片),它是能够增加和收缩的动态序列,slice 功能也更灵活。面试

数组的每一个元素能够经过索引下标来访问,索引下标的范围是从 0 开始到数组长度减 1 的位置。内置的 len 函数将返回数组中元素的个数。数组

var a [3]int             // array of 3 integers
fmt.Println(a[0])        // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
复制代码

默认状况下,数组的每一个元素都被初始化为元素类型对应的零值,对于数字类型来讲就是 0。markdown

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
复制代码

若是在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。所以,上面 q 数组的定义能够简化为:数据结构

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
复制代码

数组的长度是数组类型的一个组成部分,所以[3]int 和[4]int 是两种不一样的数组类型。架构

数组的长度必须是常量表达式,由于数组的长度须要在编译阶段肯定。app

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
复制代码

若是一个数组的元素类型是能够相互比较的,那么数组类型也是能够相互比较的,这时候咱们能够直接经过==比较运算符来比较两个数组,只有当两个数组的全部元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循一样的规则。函数

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
复制代码

2. 切片(Slice)

Slice(切片)表明变长的序列,序列中每一个元素都有相同的类型。一个 slice 类型通常写做[]T,其中 T 表明 slice 中元素的类型;slice 的语法和数组很像,只是没有固定长度而已。post

一个 slice 是一个轻量级的数据结构,提供了访问数组子序列(或者所有)元素的功能,并且 slice 的底层确实引用一个数组对象。学习

一个 slice 由三个部分构成:指针、长度和容量。

  • 指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不必定就是数组的第一个元素。

  • 长度对应 slice 中元素的数目;

  • 长度不能超过容量,容量通常是从 slice 的开始位置到底层数据的结尾位置。内置的 len 和 cap 函数分别返回 slice 的长度和容量。

表示一年中每月份名字的字符串数组,还有重叠引用了该数组的两个 slice。数组这样定义:

months := [...]string{1: "January", /* ... */, 12: "December"}
复制代码

所以一月份是 months[1],十二月份是 months[12]。

一般,数组的第一个元素从索引 0 开始,可是月份通常是从 1 开始的,所以咱们声明数组时直接跳过第 0 个元素,第 0 个元素会被自动初始化为空字符串。

slice 的切片操做 s[i:j],其中 0 ≤ i≤ j≤ cap(s),用于建立一个新的 slice,引用 s 的从第 i 个元素开始到第 j-1 个元素的子序列。新的 slice 将只有 j-i 个元素。若是 i 位置的索引被省略的话将使用 0 代替,若是 j 位置的索引被省略的话将使用 len(s)代替。所以,months[1:13]切片操做将引用所有有效的月份,和 months[1:]操做等价;months[:]切片操做则是引用整个数组。让咱们分别定义表示第二季度和北方夏天月份的 slice,它们有重叠部分:

Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2)     // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
复制代码

append 函数

append 函数用于向 slice 追加元素:

var runes []rune
for _, r := range "Hello, 世界" {    
       runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
复制代码

为了提升内存使用效率,新分配的数组通常略大于保存 x 和 y 所须要的最低大小。经过在每次扩展数组时直接将长度翻倍从而避免了屡次内存分配,也确保了添加单个元素操做的平均时间是一个常数时间。这个程序演示了效果:

func main() {    
    var x, y []int    
    for i := 0; i < 10; i++ {        
        y = appendInt(x, i)        
        fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)        
        x = y    
    }
}

//每一次容量的变化都会致使从新分配内存和copy操做:
0  cap=1    [0]
1  cap=2    [0 1]
2  cap=4    [0 1 2]
3  cap=4    [0 1 2 3]
4  cap=8    [0 1 2 3 4]
5  cap=8    [0 1 2 3 4 5]
6  cap=8    [0 1 2 3 4 5 6]
7  cap=8    [0 1 2 3 4 5 6 7]
8  cap=16   [0 1 2 3 4 5 6 7 8]
9  cap=16   [0 1 2 3 4 5 6 7 8 9]
复制代码

让咱们仔细查看 i=3 次的迭代。当时 x 包含了[0 1 2]三个元素,可是容量是 4,所以能够简单将新的元素添加到末尾,不须要新的内存分配。而后新的 y 的长度和容量都是 4,而且和 x 引用着相同的底层数组,如图 4.2 所示。

在下一次迭代时 i=4,如今没有新的空余的空间了,所以 appendInt 函数分配一个容量为 8 的底层数组,将 x 的 4 个元素[0 1 2 3]复制到新空间的开头,而后添加新的元素 i,新元素的值是 4。新的 y 的长度是 5,容量是 8;后面有 3 个空闲的位置,三次迭代都不须要分配新的空间。当前迭代中,y 和 x 是对应不一样底层数组的 view。此次操做如图 4.3 所示。

内置的 append 函数可能使用比 appendInt 更复杂的内存扩展策略。

所以,一般咱们并不知道 append 调用是否致使了内存的从新分配,所以咱们也不能确认新的 slice 和原始的 slice 是否引用的是相同的底层数组空间。

一样,咱们不能确认在原先的 slice 上的操做是否会影响到新的 slice。

做者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专一架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一块儿成长。

关注「架构精进之路」公众号并回复“01”,送你一份程序员成长进阶大礼包,另外面试宝典、大量技术电子书免费领取。

Thanks for reading!

相关文章
相关标签/搜索