Go 切片

Go 切片
创建于:2020年08月13日 更新于:2020年08月16日

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

数组生成切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

从连续内存区域生成切片是常见的操作,格式如下:
slice [开始位置 : 结束位置],切片结果不包括结束位置

从数组生成切片:

package main

import "fmt"

func main() {
    var a  = [5]int{1, 2, 3, 4, 5}
    fmt.Println(a[1:3]) // [2 3]
}

从数组或切片生成新的切片拥有如下特性:

  • 切片位置为:0 - len(数组);
  • 取出元素不包含结束位置对应的索引;
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:var name []Type,其中 name 表示切片的变量名,Type 表示切片对应的元素类型。

package main

import "fmt"

func main() {
    // 声明字符串切片
    var strList []string

    // 声明整型切片
    var numList []int

    // 声明一个空切片
    var numListEmpty = []int{}

    // 输出3个切片
    fmt.Println(strList, numList, numListEmpty) // [] [] []

    // 输出3个切片大小
    fmt.Println(len(strList), len(numList), len(numListEmpty)) // 0 0 0

    // 切片判定空的结果
    fmt.Println(strList == nil) // true
    fmt.Println(numList == nil) // true
    fmt.Println(numListEmpty == nil) // false
}

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []Type, size, cap )

其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

package main

import "fmt"

func main() {
    a := make([]int, 2)
    b := make([]int, 2, 10)

    fmt.Println(a, b) // [0 0] [0 0]
    fmt.Println(len(a), len(b)) // 2 2
}

其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。

容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

append()

Go语言的内建函数 append() 可以为切片动态添加元素

package main

import "fmt"

func main() {
    var a []int
    a = append(a, 1) // 追加1个元素
    a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
    a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
    fmt.Println(a) // [1 1 2 3 1 2 3]
}

除了在切片的尾部追加,我们还可以在切片的开头添加元素

package main

import "fmt"

func main() {
    var a = []int{1,2,3}
    a = append([]int{0}, a...) // 在开头添加1个元素
    a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
    fmt.Println(a) // [-3 -2 -1 0 1 2 3]
}

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

从切片中删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

删除头部元素

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}
    slice = slice[1:] // 删除开头1个元素
    fmt.Println(slice) // [2 3]
}

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}
    slice = append(slice[:0], slice[1:]...) // 删除开头1个元素

    fmt.Println(slice) // [2 3]
}

删除尾部元素

package main

import "fmt"

func main() {
    // 删除尾部一个元素

    slice := []int{1, 2, 3, 4, 5, 6}
    // slice = slice[:len(slice)-1]
    slice = append(slice[:len(slice)-1], slice[:0]...)

    fmt.Println(slice)
}

删除任意位置元素

package main

import "fmt"


func main() {
    slice := []int{1, 2, 3, 4, 5, 6}

    index := 3

    slice = append(slice[:index], slice[index+1:]...)

    fmt.Println(slice)
}
package main

import "fmt"

func main() {
    a := []int{1, 2, 3, 4, 5}
    b := a[1:4]

    c := b[2:4]

    fmt.Println(c) // [4 5]
}

Reslice

  • Reslice时索引以被slice的切片为准
  • 索引不可以超过被slice的切片的容量cap()值
  • 索引越界不会导致底层数组的重新分配而是引发错误

Append

  • 可以在slice 尾部追加元素
  • 可以将一个slice追加在另一个slice尾部
  • 如果最终长度未超过追加到slice的容量则返回原始slice
  • 如果超过追加到的slice的容量则将重新分配数组并拷贝原始数据