Skip to content

6-切片

切片是一个拥有相同数据类型元素的可变长度的序列。

数组是 固定 长度,切片是 可变 长度 数组是 值类型,切片是 引用类型

数组有很多局限性,切片非常灵活,支持自动扩容

切片内部结构包含 地址长度容量,一般用于快速操作一块数据集合。

创建切片

注意

切片是引用类型!!!

1
2
3
4
5
6
7
8
// 切片
var idn []T
var idn = []T{initial value}
idn := []T{initial value}

// 数组
var idn [len]T
var idn = [...]T{initial value}
idn:切片名、T:切片数据类型

区别于数组,切片在定义时不用填写 len。

它和初始化数组时省略 len 不同,数组省略 len 时要写 ...,切片啥也不用写。

Example

1
2
3
4
5
6
7
8
func main() {
    var a []string               // 声明一个字符串切片
    var b = []int{1, 2, 3, 4, 5} // 声明一个整型切片,并初始化
    c := []bool{false, true}     // 声明一个布尔型切片,并初始化
    fmt.Println(a)               // []
    fmt.Println(b)               // [1 2 3 4 5]
    fmt.Println(c)               // [false, true]
}

Slice 的创建方式有三种:

  1. 通过下标的方式获得数组或切片的一部分
  2. 使用字面量初始化新的切片
  3. 使用关键字 make 创建切片
1
2
3
4
5
arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8}

s1 := arr[2:6]              // 1. 通过下标基于数组或切片创建
s2 := []int{11, 22, 33}     // 2. 通过字面量创建
s3 := make([]int, 10, 20)   // 3. 通过关键字 make 创建

基于数组创建

切片底层是数组,当底层数组不够的时候,切片就会扩容。

上面的定义是创建一个匿名数组,让切片指向这个匿名数组,

下面是基于数组定义切片: idn := array[start_with:end:max]

func main() {
    // 基于数组定义切片
    arr := [8]int{55, 56, 57, 58, 59, 60, 61, 62}
    b := arr[1:5]          // 其范围用数学表示为:[1,5) 从arr[1]取到arr[4]不包含arr[5]
    fmt.Println(b)         // [55 56 57 58 59]
    fmt.Printf("%T \n", b) // []int

    // 切片再次切片
    c := b[0:len(b)]       // len(b)为5,所以取了b[0]、b[1]、b[2]、b[3]、b[4],相当于复制一整个切片
    fmt.Println(c)         // [55 56 57 58 59]
    fmt.Printf("%T \n", c) // []int
}

直接定义切片指定数组定义切片 的区别在于:

  1. 直接定义切片会引用一个匿名数组,指定数组定义切片会引用指定数组
  2. 直接定义切片长度=容量,指定数组定义切片长度和容量视具体情况

直接定义切片 ↓ 直接定义切片

指定数组定义切片 ↓ 指定数组定义切片

指定数组定义切片

容量指的是从接片第一个元素到底层数组的最后一个元素

例如上面第三张图中:

Example

1
2
3
4
5
arr := [8]int{55, 56, 57, 58, 59, 60, 61, 62}
c := arr[3:6]

len(c)    // 3
cap(c)    // 5

使用make()创建

如果需要 动态 的创建一个切片,可以使用内置的make()函数 基本语法为:

1
2
3
4
make([]T, len, cap)

var idn []T = make([]T, len, cap)
idn := make([]T, len, cap)
T:切片数据类型、len:长度、cap:容量 cap 可以不填,默认和 len 相同

func main() {
    // make函数构造切片
    // make([]T, len, cap)

    d := make([]int, 4, 10) // 构造一个整型切片,填充5个元素,最大容量10
    fmt.Println(d)          // [0 0 0 0]
    fmt.Printf("%T \n", d)  // []int
    fmt.Println(len(d))     // 4
    fmt.Println(cap(d))     // 10
}

动态就动态在于它能使用变量哈哈哈

1
2
3
func fn(a int, b int) []int {
    return make([]int, a, b)
}

切片是引用类型

判空

检查切片是否为空,不能用 s == nil,而是应该使用 len(s) == 0

Note

切片是一种引用类型,当它被声明的时候,没有指向任何数组,包括匿名数组也没有,此时切片中指针为 nil

var s []int    // s == nil

当切片被初始化的时候,它就指向了一个数组,这时切片中的指针不为 nil

var s = []int{}    // s != nil

切片不能直接比较

切片是一种引用类型,我们不能用 == 操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和 nil 比较。

一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。 但是我们不能说一个长度和容量都是0的切片一定是nil

1
2
3
var s1 []int         //len(s1)=0; cap(s1)=0; s1==nil
s2 := []int{}        //len(s2)=0; cap(s2)=0; s2!=nil
s3 := make([]int, 0) //len(s3)=0; cap(s3)=0; s3!=nil
所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

切片的拷贝赋值

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

Example

1
2
3
4
5
6
7
func main() {
    s1 := make([]int, 3) //[0 0 0]
    s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组
    s2[0] = 100
    fmt.Println(s1) //[100 0 0]
    fmt.Println(s2) //[100 0 0]
}

切片遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

func main() {
    s := []int{1, 3, 5}

    for i := 0; i < len(s); i++ {
        fmt.Println(i, s[i])
    }

    for index, value := range s {
        fmt.Println(index, value)
    }
}

append()

Go 中的内建函数 append() 可以为切片动态添加元素,可以一次添加一个或多个元素。

append(slice, elem_arr_or_slice)

eg:

1
2
3
4
5
func main() {
    s := []int{1, 2, 3, 4, 5}   // [1 2 3 4 5]
    s = append(s, 11)           // [1 2 3 4 5 11]
    s = append(s, 12, 13, 14)   // [1 2 3 4 5 11 12 13 14]
}

append() 第二个参数也可以是另一个切片,不过记得加上 ...

Example

1
2
3
4
func main() {
    s2 := []int{55, 56, 57}
    s = append(s, s2...)
}

注意: 通过var声明的零值切片可以在 append() 函数直接使用,无需初始化。

// √ 正确
var s []int
s = append(s, 1, 2, 3)

// X 没必要
var s = []int{}
s = append(s, 1, 2, 3)
// X 没必要
var s = make([]int)
s = append(s, 1, 2, 3)

copy()

切片是引用类型,如果直接 s1 = s2 ,其实是将 s2 中的数组地址赋值给 s1 的指针,s1 修改的时候 s2 也会受影响。 要实现真正的复制,需要使用内建函数 copy() 进行复制。

copy(destSlice, srcSlice)
destSlice:目标切片、srcSlice:数据来源切片

func main() {
    s1 := []int{1, 2, 3, 4}
    s2 := []int{12, 13, 14}
    copy(s1, s2)
    fmt.Println(s1)    // [12 13 14 4]
    fmt.Println(s2)    // [12 13 14]

    s3 := []int{1, 2, 3, 4}
    s4 := []int{12, 13, 14}
    copy(s4, s3)
    fmt.Println(s3)    // [1 2 3 4]
    fmt.Println(s4)    // [1 2 3]

    s5 := []int{1, 2, 3, 4}
    s6 := make([]int, 4)
    copy(s5, s6)
    fmt.Println(s5)    // [0 0 0 0]
    fmt.Println(s6)    // [0 0 0 0]

    s7 := []int{1, 2, 3, 4}
    s8 := make([]int, 4)
    copy(s8, s7)
    fmt.Println(s7)    // [1 2 3 4]
    fmt.Println(s8)    // [1 2 3 4]
    s8[0] = 100
    fmt.Println(s7)    // [1 2 3 4]
    fmt.Println(s8)    // [100 2 3 4]
}

[:] 语法

Note

这里需要说明一下,无论任何语言的 [:] 语法都是左闭右开的,用数学表达就是 \([start, end)\),因为这样写起来会很方便。

举个例子:当我想要基于某个数组,从下标 2 切到末尾结束,那么写起来就是这样的:

var arr [8]{0, 1, 2, 3, 4, 5, 6, 7}
s := arr[2:len(arr)]
因为下标是从 0 开始的,而 len() 的计算是从 1 开始的,也就是说数组 arr 的长度是 8,下标是从 0 ~ 7.

那么 s:= arr[2:len(arr)] 就相当于 s := arr[2:8]

下标是 8 就已经越界了,但是 [:] 语法是左闭右开,所以就是取 arr 的下标 2 到下标 7。

在 Python 中,[:]的语法规则是 [low:high:step]

而在 Golang 中则是 [low:high:max],其规则是:

0 <= low <= len(arr) <= high <= max <= cap(arr)

  • low的取值在 0 至 底层结构长度,取底层结构长度时为空切片
  • high的取值在low 至 max,取 low 时为空切片
  • max的取值在high 至 底层结构容量,取 high 时新切片 len==cap

举个栗子:

func main() {
    a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s := a[2:6:10]  // [low:high:max]

    fmt.Printf("a: %v\n", a)          // a: [0 1 2 3 4 5 6 7 8 9]
    fmt.Printf("a len: %d\n", len(a)) // a len: 10
    fmt.Printf("a cap: %d\n", cap(a)) // a cap: 10

    fmt.Printf("s: %v\n", s)          // s: [2 3 4 5]
    fmt.Printf("s len: %d\n", len(s)) // s len: 4
    fmt.Printf("s cap: %d\n", cap(s)) // s cap: 8
}

总结一下就是

  • 新切片的容量 cap(s) = max - low
  • 新切片的长度 len(s) = high - low
  • low 取值在 [0, len(arr)]
  • high 取值在 [low, max]
  • max 取值在 [hign, cap(arr)]

删除

Go 并没有提供删除切片元素的方法,我们可以利用其本身的特性来删除元素 切片可以取自切片,那我们就将被删除元素之前的元素切下来,再把被删除元素之后的元素切下来,然后用append()拼接

1
2
3
4
5
6
func main() {
    a := []int{30, 31, 32, 33, 34, 35, 36, 37}
    // 删除下标为2的元素
    a = append(a[:2], a[3:]...)
    fmt.Println(a)    // [30, 31, 33, 34, 35, 36, 37]
}
简单说就是 a = append(a[:index], a[index+1:]...)

其他操作

// append slice
slice = append(slice, slice2...)

// copy
dest := make([]int, len(src))
copy(dest, src)

// 删除多个连续的元素
a = append(a[:i], a[j:]...)

// 删除一个元素 i
a = append(a[:i], a[i+1:]...)

// 扩展 j 个元素
a = append(a, make([]T, j)...)

// 插入元素 x 到 i 的位置上
a = append(a[:i], append([]T{x}, a[i:]...)...)

// push,追加到尾部
a = append(a, x)

// pop,尾部弹出
x, a = a[len(a)-1], a[:len(a)-1]

// dequeue,队头出队
x, a = a[0], a[1:]

// enqueue,队尾入队
a = append(a, x)


// shift,头部取出
x, a = a[0], a[1:]

// unshift,头部加入
a = append([]T{x}, a...)

总结

切片和数组 之间有那么点像数据库中的 基本表和视图

基本表是存储数据定义和数据的,视图只存储数据定义。所以基本表改变时视图也改变。

数组是存储定义和数据的,切片只存储定义。所以数组元素改变时切片也改变。

这个例子可能不是很贴切,但联系一下这句话:数组是值类型,切片是引用类型。 也就是说,数组是实实在在存数据的地方,切片只是对某个数组的引用,自己并没有数据。