浅谈Go语言(3) - 数组、切片与链表

本文深入探讨Go语言中的数组、切片、双向链表和环形链表,解析每种数据结构的特点、应用场景及内部机制,通过代码示例展示切片的动态扩展、链表的开箱即用特性。

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]

  对以上内容,画图分析

image

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代码包中。

  代码包中有两个公开的程序实体: ListElementList实现了一个双向链表(以下简称链表),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