我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型。
共同点
都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素)。 最重要的不同 数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。
数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型声明定义的一部分。比如,
[1]string和[2]string就是两个不同的数组类型。而切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。
(类型字面量、值字面量)
从底层看二者关系
我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
值类型与引用类型角度区分
也正因为如此,Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。
总结一下:引用类型有:切片类型、字典类型、通道类型、函数类型;值类型有:数组类型、基础数据类型、结构体类型
判断传值传引用
注意,Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。
如果传递的值是引用类型的,那么就是“传引用”(符号&);
如果传递的值是值类型的,那么就是“传值”(符号=);
如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
索引|切片共性
我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片。
长度差别
我们通过调用内建函数
len,得到数组和切片的长度。通过调用内建函数cap,我们可以得到它们的容量。 但要注意,数组的容量永远等于其长度,都是不可变的。切片的容量却不是这样,并且它的变化是有规律可寻的。
今天的问题就是:怎样正确估算切片的长度和容量?
比较容易理解,故此处不展开。
知识扩展
问题 1:怎样估算切片容量的增长?
以长度1024,为界限,2倍,1.25倍。更多细节可参见
runtime包中 slice.go 文件里的growslice及相关函数的具体实现。 问题 2:切片的底层数组什么时候会被替换? 确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。
在无需扩容时,
append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。只要新长度不会超过切片的原容量,使用
append函数追加元素的时候就不会引起扩容。只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。
思考题
1.如果有多个切片指向了同一个底层数组,那么你认为应该注意些什么?
当操作其中一个切片的时候大概率会影响到其他指向同一个底层数组的切片。
要么彻底切断这些切片的底层联系,要么立即为所有的相关操作加锁。 可以使用copy函数,重新创建一个切片,不影响源切片。
number5 := make([]int, 2) copy(number5, numbers[:2])
2.怎样沿用“扩容”的思想对切片进行“缩容”?请写出代码。
如果你需要频繁的“缩容”,那么就可能需要考虑其他的数据结构了,比如:container/list代码包中的List。
or 对切片再次切片,缩小起止范围,就可以缩容
总结
切片的特点:由于是引用类型,传递成本小、创建便捷,可以通过“窗口”快速地定位并获取,并可以修改底层数组中的元素。
缺点1:删除切片中的元素就会造成大量元素的移动,注意空出的元素位置要“清空”,否则会造成内存泄露。
缺点2:切片频繁“扩容”,底层数组不断产生,内存分配的量以及元素复制的次数也会越来越多,影响运行性能。
如果没有一个合理、有效的切片“缩容”策略(旧底层数组无法回收,新底层数组不断产生),会导致内存的过度浪费,降低程序运行性能,使得内存溢出,并可能导致程序崩溃。