文章目录
1. 写在前面
对于拿着锤子的人来讲,全世界都是钉子。-- by 查理·芒格
任何数据结构,自身特点和适用场景都非常鲜明,上面介绍的都是 Go 语言原生的数据结构,使用起来也都很方便。能否用好,取决于大家对其内部原理机制的理解是否足够深刻。
2. 数组与切片
数组的长度是固定的,切片是可变长的。
(1) 数组
数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,[1]string和[2]string就是两个不同的数组类型。
[3]string{"a","b","c"} // 数组 array
(2) 切片
切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。
[]string{"a","b","c"} // 切片 slice
相关性
切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。
数组可以被叫做切片的底层数组,切片也可以被看作是对数组的某个连续片段的引用。
- 数组为值类型
- 切片为引用类型
从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。
代码浅析
- 内建函数
len
,获取数组和切片的长度。 - 内建函数
cap
,获取数组和切片的容量。
接下来我们来看代码
package main
import "fmt"
func main() {
s1 := make([]int, 5)
printSlice(s1)
s2 := make([]int, 5, 8)
printSlice(s2)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d value:%v\n", len(s), cap(s), s)
}
执行结果:
len=5 cap=5 value:[0 0 0 0 0]
len=5 cap=8 value:[0 0 0 0 0]
make
函数初始化切片时,如不指明其容量,那么容量就会和长度一致。
append
下面我们来看一段更加典型的代码:
func main() {
s2 := make([]int, 5, 8)
printSlice(s2)
s2 = append(s2, 6)
s2 = append(s2, 7)
s2 = append(s2, 8)
printSlice(s2)
s2 = append(s2, 9)
printSlice(s2)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d value:%v\n", len(s), cap(s), s)
}
执行结果:
len=5 cap=8 value:[0 0 0 0 0]
len=8 cap=8 value:[0 0 0 0 0 6 7 8]
len=9 cap=16 value:[0 0 0 0 0 6 7 8 9]
这段代码说明什么问题呢?
- 当切片长度超过了容量,容量会扩展到原来的2倍(虽然这不是一定的)。
扩容的内部原理:
- 扩容后不会改变原来的切片,会生成一个容量更大的切片,将原有的元素和新元素一起拷贝到新切片中。
关于扩容的2倍问题:
- 当原切片的长度大于或等于1024时,扩容将会以原容量的1.25倍作为新容量的基准
切片的一般操作
下面通过代码进行切片操作
func main() {
s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6] // [0] notice: [3, 6)
printSlice(s4)
s := s3[:0] // [1] 截取切片使其长度为 0
printSlice(s)
s = s3[:4] // [2] 拓展其长度
printSlice(s)
s = s3[2:] // [3] 舍弃前两个值
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d value:%v\n", len(s), cap(s), s)
}
执行结果:
len=3 cap=5 value:[4 5 6]
len=0 cap=8 value:[]
len=4 cap=8 value:[1 2 3 4]
len=6 cap=6 value:[3 4 5 6 7 8]
对以上内容,画图分析
copy
继续看代码
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{6, 7, 8}
slice3 := []int{9, 10, 11}
copy(slice2, slice1) // [4]
printSlice(slice2)
copy(slice1, slice3) // [5]
printSlice(slice1)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d value:%v\n", len(s), cap(s), s)
}
执行结果:
len=3 cap=3 value:[1 2 3]
len=5 cap=5 value:[9 10 11 4 5]
解析:如果两个切片不一样大,会按其中较小的那个数组切片的元素个数进行复制。
代码[4]中,copy(slice2, slice1)
,只会复制slice1的前3个元素到slice2中
代码[5]中,copy(slice1, slice3)
,只会复制slice3的3个元素到slice1的前3个位置
(3) 切片与数组的比较
切片本身有着占用内存少和创建便捷等特点,它本质上还是数组。
删除切片中的元素是很麻烦的,涉及到元素复制、移动、槽位清空,否则还会内存泄漏。
切片频繁扩容,底层会进行内存分配和元素复制,影响性能。
当我们没有一个合理、有效的”缩容“策略的时候,旧的底层数组无法被回收,新的底层数组中也会有大量无用的元素槽位。过度的内存浪费不但会降低程序的性能,还可能会使内存溢出并导致程序崩溃。
3. container包中的标准容器
(1) List双向链表
Go 语言的链表实现在标准库的container/list
代码包中。
代码包中有两个公开的程序实体: List
和Element
,List
实现了一个双向链表(以下简称链表),Element
则代表了链表中元素的结构。
内置函数
// type Element
type Element struct {
// 在元素中存储的值
Value interface{}
}
// 返回该元素的下一个元素,如果没有下一个元素则返回nil
func (e *Element) Next() *Element
// 返回该元素的前一个元素,如果没有前一个元素则返回nil。
func (e *Element) Prev() *Element
// type List
// 返回一个初始化的list
func New() *List
// 获取list l的最后一个元素
func (l *List) Back() *Element
// 获取list l的第一个元素
func (l *List) Front() *Element
// list l初始化或者清除list l
func (l *List) Init() *List
// 在list l中元素mark之后插入一个值为v的元素,并返回该元素,如果mark不是list中元素,则list不改变。
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
// 在list l中元素mark之前插入一个值为v的元素,并返回该元素,如果mark不是list中元素,则list不改变。
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
// 获取list l的长度
func (l *List) Len() int
// 将元素e移动到元素mark之后,如果元素e或者mark不属于list l,或者e==mark,则list l不改变。
func (l *List) MoveAfter(e, mark *Element)
// 将元素e移动到元素mark之前,如果元素e或者mark不属于list l,或者e==mark,则list l不改变。
func (l *List) MoveBefore(e, mark *Element)
// 将元素e移动到list l的末尾,如果e不属于list l,则list不改变。
func (l *List) MoveToBack(e *Element)
// 将元素e移动到list l的首部,如果e不属于list l,则list不改变。
func (l *List) MoveToFront(e *Element)
// 在list l的末尾插入值为v的元素,并返回该元素。
func (l *List) PushBack(v interface{}) *Element
// 在list l的尾部插入另外一个list,其中l和other可以相等。
func (l *List) PushBackList(other *List)
// 在list l的首部插入值为v的元素,并返回该元素。
func (l *List) PushFront(v interface{}) *Element
// 在list l的首部插入另外一个list,其中l和other可以相等。
func (l *List) PushFrontList(other *List)
// 如果元素e属于list l,将其从list中删除,并返回元素e的值。
func (l *List) Remove(e *Element) interface{}
代码示例:
package main
import (
"container/list"
"fmt"
)
func main() {
l := list.New() //创建一个新的list
for i := 1; i < 5; i++ {
l.PushBack(i