Go切片(slices)

Go语言的切片(slices),可以高效且方便的处理同类型的数据序列

数组

在了解切片之前需要先了解数组

一个数组的定义需要指定长度和元素类型

比如说,[4]int 表示一个存储四个整数的数组。 数组的长度是固定的。长度是数组类型的一部分,因此 [4]int[5]int 是完全不同的类型。 数组可以使用索引方式访问,表达式 s[n] 访问数组的第 n 个元素(从0开始计数)

var a [4]int
a[0] = 1
i := a[0]
// i == 1

在声明数组时,数组元素会自动初始化为其对应类型的零值

// a[2] == 0

数组在内存中是连续存储的,比如说,数组 [4]int 对应内存中四个连续的整数

在这里插入图片描述

Go的数组变量表示整个数组,而在C语言中,它是指向数组中第一个元素的指针。这就意味着,当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。为了避免这样的拷贝,可以利用数组指针进行传递。可以将数组看作一个特殊的结构体,结构的字段名对应数组的索引,同时成员的数目固定

利用字面量声明数组:

b := [2]string{"Penn", "Teller"}

让编译器统计数组字面值中元素的数目:

b := [...]string{"Penn", "Teller"}

这两种写法得到的结果都是一样的

切片

切片是建立在数组之上的抽象数据类型,使用方便,功能强大

切片类型的写法是 []TT 是切片元素的类型。和数组不同的是,切片类型并没有固定的长度。

切片的字面量和数组字面量类似,不同的是切片不指定元素个数:

letters := []string{"a", "b", "c", "d"}

切片可以使用内置函数 make 创建:

func make([]T, len, cap) []T

函数 make 参数中,T表示被创建的切片元素的类型,len表示长度,cap表示容量。 调用 make 时,内部会分配一个数组,然后返回数组对应的切片。

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

当容量与长度相同时,容量参数可以忽略不写:

s := make([]byte, 5)

可以使用内置函数 lencap 获取切片的长度和容量信息:

len(s) == 5
cap(s) == 5

切片的零值为 nil 。对于切片的零值, lencap 都将返回0

通过现有的数组或则切片可以生成新的切片。切分的范围是一个左闭右开的区间。例如,表达式 b[1:4]创建的切片引用数组 b 的第2到4个元素空间

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, 共享同一块内存区域

切片的开始和结束的索引都是可选的,它们的默认值分别为零和数组的长度。

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

基于现有数组创建切片:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // 切片引用数组的内存区域

切片内部

一个切片是一个数组片段的描述

切片包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)

Go切片(slices)_第1张图片

之前使用 make([]byte, 5) 创建的切片变量 s 的结构:

Go切片(slices)_第2张图片

长度是切片引用的元素数目。容量是,从切片指针开始,底层数组的元素数目

我们继续切分切片变量s

s = s[2:4]

Go切片(slices)_第3张图片

切片操作将创建一个新的切片,并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素,会影响到原始切片的对应元素

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:] 
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

切片变量s的长度小于它的容量,因此可以增长切片的容量:

s = s[:cap(s)]

Go切片(slices)_第4张图片

切片的长度不能超过它的容量。和切片或数组的索引超出范围一样,增长超出切片的范围会导致运行时异常

增长切片(copyappend函数)

增加切片的容量,需要创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片s容量翻倍:

// 创建一个2倍容量的新切片t
t := make([]byte, len(s), (cap(s)+1)*2) // +1操作是处理cap(s)==0的情况
// 复制`s`的元素到`t` 
for i := range s {
        t[i] = s[i]
}
// 将`t`赋值给`s` 
s = t

循环中复制的操作可以由内置函数copy替代。copy函数将源切片的元素复制到目的切片。 它返回复制元素的数目

func copy(dst, src []T) int

当源和目的的切片长度不同时,copy函数只复制,较短切片的长度个元素。 此外, copy函数可以正确处理源和目的切片有重叠的情况。

使用copy函数来优化上面将切片s容量翻倍的例子:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

另一个常见的操作是将数据追加到切片的尾部。下面的函数,将元素追加到切片尾部,必要的话会增加切片的容量,最后返回更新后的切片:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // 需要的空间大于现有的空间
        // 为了将来可能的增长,分配更大的内存
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

使用AppendByte函数:

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

AppendByte函数对切片的的增长方式控制性很强,类似这样的函数,可以根据程序特点,灵活的分配内存

Go语言提供了一个内置的append函数,适用于大多数场合

func append(s []T, x ...T) []T

append函数将一系列元素x追加到切片s的末尾,并且在必要的时候增加容量

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

如果是要将一个切片追加到另一个切片尾部,需要使用 ... 语法将第2个参数展开为参数列表。

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 相当于 `a = append(a, b[0], b[1], b[2])`
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

由于切片的零值 nil用起来就像一个长度为零的切片,可以声明一个切片变量然后在循环中向它追加数据:

// Filter returns a new slice holding only
// the elements of s that satisfy fn()
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

可能遇到的陷阱

切片操作并不会复制底层的数组。当存在引用时,整个数组将被保存在内存中

可能会因为一个小的内存引用导致保存所有的数据。

例如, FindDigits 函数将加载整个文件到内存中,然后搜索第一个连续的数字,最后结果以切片方式返回

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

这段代码执行结果于预期一样,但返回的切片指向了包含了整个文件内容的数组,因此垃圾回收器不能回收这个数组。这就导致了较小有用的的信息却占用了很大的内存空间

我们可以复制有用的信息到一个新的切片里来解决这个问题

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

原文链接

https://blog.golang.org/go-slices-usage-and-internals

你可能感兴趣的:(Go切片(slices))