[Golang学习笔记] 07 数组和切片
01-06回顾:
Go语言开发环境配置,
常用源码文件写法,
程序实体(尤其是变量)及其相关各种概念和编程技巧:
类型推断,变量重声明,可重名变量,类型推断,类型转换,别名类型和潜在类型
数组:
数组类型的值的长度是固定的,在声明数组的时候,长度必须给定,并且在之后不会改变,可以说数组的长度是其类型的。
比如:[1]string和[2]string就是两个不同的数组类型。
切片:
切片类型的值是可变长的。切片的类型字面量中只有元素的类型([]int),而没有长度。切片的长度可以自动地随着其中元素数量的增长而增加,但是不会随着元素数量的减少而减小。可以把切片看作是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。后者被叫做前者的底层数组,前者被看作是对后者的某个连续片段的引用。
最大区别:数组长度是固定的,切片是可变长的。
数组可以叫切片的底层数组,切片可以看作数组的连续片段的引用。
定义一个切片(三种方式):
①定义一个切片,然后让切片去引用一个已经创建好的数组
var arr [5]int = [5]int {1, 2, 3, 4, 5} var slice = arr[1:3]
②通过make来创建切片。基本语法:var 切片名 []type = make([], len, [cap]);参数说明:type是数据类型、len是大小、cap是切片容量(容量必须>=长度)
var slice []float64 = make([]float64, 5, 10)
- 通过make方式创建切片可以指定切片大小和容量
- 如果没有给切片的各个元素赋值,那么就会使用默认值(int、float=>0, strint=>"", bool=>false)
- 如果make方式创建的切片对应的数组是由make底层维护,对外不可见,也就是只能通过slice访问各个元素
③定义一个切片,直接就指定具体数组,使用原理类似于make的方式
var slice []string = []string{"zhangsan", "lisi", "wangwu"}
第一种方式是直接引用数组,这个数组是事先存在的,程序员可见
第二种方式是通过make来创建切片,make也会创建一个数组,是由切片在底层维护,程序员不可见
切片长度和容量计算公式:
对底层数组容量是 k 的切片 slice[i:j] 来说:
长度: j - i
容量: k - i
扩展:
Go语言引用类型:切片类型,字典类型,通道类型,函数类型。
值类型:基础数据类型,数组类型,结构体类型。
如果传递的值是引用类型的,那么就是“传引用”(符号&);
如果传递的值是值类型的,那么就是“传值”(符号=);
从传递成本的角度讲,引用类型(&)的值要比值类型的值低很多。
面试题:怎样正确估算切片的长度和容量?
// demo15.go package main import ( "fmt" ) func main() { s1:=make([]int, 5)// 指明长度 fmt.Printf("The value of s1 is: %v\n", s1) fmt.Printf("The length of s1 is: %d\n", len(s1)) fmt.Printf("The capacity of s1 is: %d\n", cap(s1)) fmt.Printf("The value of s1 is: %d\n", s1) s2:=make([]int, 5, 8)// 指明长度和容量 fmt.Printf("The length of s2:%d\n", len(s2)) fmt.Printf("The capacity of s2 is:%d\n", cap(s2)) fmt.Printf("The value of s2 is:%d\n", s2) }
切片s1和s2的容量分别是5和8。
问题分析:
切片的容量实际上代表它底层数组的长度。切片的底层数组长度是不可变的。
想象有一个窗口,通过这个窗口可以看到一个数组,但是不一定能看到该数组中所有元素,有一部分是被挡住了,有时候只能看到连续的一部分元素。
该例中,这个数组就是切片s2的底层数组,而这个窗口就是s2本身。s2的长度就是这个窗口的宽度,决定了你透过s2可以看到其底层数组中的哪几个连续的元素。
由于s2的长度是5(定义的),所以你可以看到其底层数组中的第1到第5个元素,对应的底层数组的索引范围是0到4([0,4])。
切片代表的窗口被划分成一个个的小格子,每个小格子都对应着其底层数组中的某一个元素。
s2中,窗口最左边的小格子对应的正好是其底层数组中的第一个元素,即索引为0的那个元素。因此,s2中的索引从0到4的元素,就是其底层数组中索引从0到4的那5个元素。
------
s3 := []int{1,2,3,4,5,6,7,8}
s4 := s3[3:6]
长度:
切片s3中有8个元素(整数1到8),s3的长度和容量都是8.
s4从s3中通过切片表达式初始化而来,其中[3:6]表达的就是透过新窗口能看到的,用减法可以计算出长度,6减去3为3.而索引范围为从3到5(不包括6,可以引申为区间表示法,即[3,6))。
s4中的索引从0到2,指向的元素对应的是s3中索引从3(起始索引)到5(结束索引)的3(长度)个元素。
容量:
切片的容量代表了它的底层数组的长度,但这仅限于用make函数或者切片字面量([]int{1,2,3})初始化切片的情况。
一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。
s4是通过在s3上施加切片操作得来的,所以s3的底层数组等于s4的底层数组;而且,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾,所以s4的容量就是其底层数组的长度8减去上述切片表达式([3:6])中的起始索引3,等于5。
要注意的是,切片代表的窗口是无法向左扩展的,也即无法透过s4看到s3中最左边的那3个元素。
切片和数组关系:
一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。
扩展
问题1: 怎么样估算切片容量的增长?
如果一个已定义的切片无法容纳更多的元素,Go就会自动为其扩容。但是,扩容不会改变原来的切片,而是会生成一个容量更大的切片,然后把原切片元素和新元素都复制到新切片中。
对于扩容的计算方法,有两种:
1、一般情况下,新容量是原容量的2倍;
2、当原长度>=(大于或等于)1024时,新容量基准不再是2倍,而是1.25倍。新容量基准会在此基础上不断地被调整,直到不小于原长度与要追加的元素数量之和(新长度),最终,新容量往往会比新长度大一些,也有可能相等。
特殊情况:如果一次追加的元素过多,以至于使得新长度比原容量的2倍还要大,那么新容量就会以新长度为基准,最终的新容量往往比新容量基准要更大一些。
扩容append注意点:
append操作可能会导致原本使用同一个底层数组的两个Slice变量变为使用不同的底层数组。
package main import "fmt" func main() { var array = []int{1, 2, 3, 4, 5} // len:5,capacity:5 var newArray = array[1:3] // len:2,capacity:4 (已经使用了两个位置,所以还空两位置可以append) fmt.Printf("array addr:%p\n", array) //0xc420098000 fmt.Printf("newArray addr:%p\n", newArray) //0xc420098008 可以看到newArray的地址指向的是array[1]的地址,即他们底层使用的还是一个数组 fmt.Printf("array value:%v\n", array) //[1 2 3 4 5] fmt.Printf("newArray value:%v\n", newArray) //[2 3] fmt.Println() newArray[1] = 9 //更改后array、newArray都改变了 fmt.Printf("array value:%v\n", array) // [1 2 9 4 5] fmt.Printf("newArray value:%v\n", newArray) // [2 9] fmt.Println() newArray = append(newArray, 11, 12) //append 操作之后,array的len和capacity不变,newArray的len变为4,capacity:4。因为这是对newArray的操作 fmt.Printf("array value:%v\n", array) //[1 2 9 11 12] //注意对newArray做append操作之后,array[3],array[4]的值也发生了改变 fmt.Printf("newArray value:%v\n", newArray) //[2 9 11 12] fmt.Println() newArray = append(newArray, 13, 14) // 因为newArray的len已经等于capacity,所以再次append就会超过capacity值, // 此时,append函数内部会创建一个新的底层数组(是一个扩容过的数组),并将array指向的底层数组拷贝过去,然后在追加新的值。 fmt.Printf("array addr:%p\n", array) //0xc420098000 fmt.Printf("newArray addr:%p\n", newArray) //0xc4200a0000 fmt.Printf("array value:%v\n", array) //[1 2 9 11 12] fmt.Printf("newArray value:%v\n", newArray) //[2 9 11 12 13 14] 他两已经不再是指向同一个底层数组了 }
问题2:切片的底层数组什么时候会被替换?
一个切片的底层数组永远不会被替换。
因为在“扩容”的时候,也一定会生成新的底层数组,同时也生成新的切片,而对原切片及其底层数组没有做任何改动。
append函数使用注意:在无需扩容时,返回的是指向原底层数组的切片;需要扩容时,返回的是指向新底层数组的新切片。
添加成员时,容量是2的指数递增的,2,4,8,16,32。而且是在长度要超过容量时,才增加容量。
只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容,这只会使得紧邻切片窗口(抽象)右边的元素被新的元素替换掉。
思考题:
1、如果有多个切片指向了同一个底层数组,应该注意什么?
当若干个切片指向同一个底层数据时,对每一个切片的修改都会反映在底层数组中。如果覆盖其他切片已经指向的数组的值。
可以使用copy函数,重新创建一个切片,不影响源切片。
number5 := make([]int, 2)
copy(number5, numbers[:2])
2、怎样沿用“扩容”的思想来对切片进行“缩容”?
对切片再次切片,缩小起止范围,就可以缩容?
总结
切片的特点:占用内存少、创建便捷,可以通过“窗口”快速地定位并获取,并可以修改底层数组中的元素。
缺点1:删除切片中的元素就会造成大量元素的移动,注意空出的元素位置要“清空”,否则会造成内存泄露。
缺点2:切片频繁“扩容”,底层数组不断产生,内存分配的量以及元素复制的次数也会越来越多,影响运行性能。
如果没有一个合理、有效的切片“缩容”策略(旧底层数组无法回收,新底层数组不断产生),会导致内存的过度浪费,降低程序运行性能,使得内存溢出,并可能导致程序崩溃。
本学习笔记仅为了总结自己学到的Go语言核心知识,方便以后回忆,文中部分内容摘录自极客时间的《Go语言核心36讲》专栏,如有侵权,请联系我删除。