Go语言中的切片类型

Go语言中的切片类型

96 大蟒传奇 关注

2016.12.18 16:02* 字数 2034 阅读 950评论 2喜欢 7赞赏 1

Go语言中的切片类型_第1张图片

图文无关

本文翻译自Andrew Gerrand的博文 https://blog.golang.org/go-slices-usage-and-internals

前言

Go语言中提供了的切片类型,方便使用者处理类型数据序列。
切片有点像其他语言中的数组,并且提供了一些额外的属性。

数组

Go语言自带了数组类型,而切片类型是基于数组类型的抽象。因此,要理解切片类型,我们必须首先理解数组。
定义一个数组时,需要指定数组长度和数组中元素的类型,比如说 [4]int定义了长度为4的数组,其中的元素类型为int。一个数组的长度是固定的;长度是数组类型的一部分([4]int[5]int就是两个不同的类型)。数组以通常的方式进行索引,所以表达式s[n]能访问到数组s的第n个元素(从0开始)。

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

在没有显式初始化时,数组默认会将元素初始化为0。

// a[2] == 0

在内存中,[4]int表示为顺序排列的4个整数值

内存中的 [4]int

Go语言中的数组是一个值。数组变量表示整个数组,而不是指向数组第一个元素的指针(就像C语言那样)。这就意味着,将一个数组当作一个参数传递时,会完全拷贝数组中的内容(如果不想完全拷贝数组,可以传一个指向数组的指针)。
可以把数组当成这样一种结构,它具有索引,有着固定的大小,可以用来存储不同类型的元素。

一个字符串数组可以这样定义

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

或者让编译器来确定数组的长度

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

上面的两个例子中,b的类型都是 [2]string

切片类型

数组类型是很有用的,但是不太灵活,所以Go代码中很少看到它们。但是切片类型却是很常见的,因为它基于数组类型提供了强大的功能和开发便利。

切片类型的定义如[]T,其中T是切片中元素的类型。与数组类型不同,切片类型没有固定的长度。

定义一个切片和定义一个数组的语法相似,唯一的不同是不需要定义切片长度。

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

可以用内置的make关键字定义一个切片

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

其中T表示切片中元素的类型。make函数接受元素类型,长度和容量(可选)作为传入参数。当被调用时,make分配一个数组,并且返回一个指向该数组的切片。

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

如果没有传入cap参数,它的默认值是传入的长度。这是上面代码的一个简洁版本。

s := make([]byte, 5)

可以使用内置的lencap函数检查切片的长度和容量。

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

下面两个章节将讨论长度和容量的关系。
切片的零值为nil。对一个值为nil的切片来说,lencap会返回0。

可以通过“切”一个数组或者是切片,来生成新的切片。这个过程通过指定两个索引的半开范围来完成,两个索引之间用冒号隔开。举个例子,b[1:4]会返回一个新的切片,包含的元素为b中的第1到第3的元素

b ;= []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}  和b中的元素占用同一块内存

起始和结束索引是可选的,其默认值分别为0和切片的长度

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

基于数组创建切片语法与上面的类似。

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:]     // s为指向x的引用

探寻切片内部

切片是数组段的描述符。它包含了一个指向数组的指针,数据段的长度和容量。

Go语言中的切片类型_第2张图片

切片结构

通过s := make([]byte, 5)方式声明的切片结构如下

Go语言中的切片类型_第3张图片

s结构

长度是切片指向内容中元素的个数。容量是底层数组中的元素个数(从切片指向的元素开始计数)。长度和容量的区别会在下面的例子中解释。

s进行切片,观察下面切片和数组的关系

s = s[2:4]

Go语言中的切片类型_第4张图片

切片和数组

切片操作并不会拷贝s中的数据,而是创建一个新的切片指向原来的数组,这让切片操作就像操作数组索引一样高效。因此,对切片的元素进行修改,会修改原始切片的元素。

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语言中的切片类型_第5张图片

切片后结果

切片的长度不能大于其容量。这样做会导致一个runtime panic,就像对切片或者数组进行越界访问一样。

增加切片容量

要增加切片的容量,必须新建一个容量更大的切片,然后将之前的切片的数据拷贝到新的切片中。这也是其他语言实现动态数组的方式。下面的例子,新建一个容量是s两倍的切片t,然后将s的数据拷贝到t中,最后将t赋值给s:

t := make([]byte, len(s)m (cap(s)+1)*2) // +1对应 cap(s) == 0的情况
for i := range s {
     t[i] = s[i]
}
s = t

使用内置的copy函数可以简化上面的代码。顾名思义,copy将数据从一个切片拷贝到另一个切片,并返回拷贝元素的数量。
语法如下:

func copy(dst, src []T) int

函数copy 支持两个不同长度切片之间的拷贝。另外,copy可以处理源和目的切片指向相同底层数组的情况,正确处理重叠的切片。

简化上面的代码

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

一个常见的操作是在切片的末尾添加一个元素。下面的函数在一个切片的末尾增加一个元素,在容量不够的情况下增加切片的容量,并且返回更新后的切片

func AppendByte(slice []byte, data ...type) []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函数就派上用场了。它的语法如下

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

函数append 将x添加到s末尾,如果需要就扩展s的容量。

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

使用...将一个切片添加到另外一个切片末尾

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

因为零值的切片(nil)和长度为0的切片相似,可以声明一个切片变量,然后在循环中在其末尾添加元素。

// 通过fn筛选出s中的元素
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)
}

上面的代码能完成所需要的功能,但是返回的[]byte切片指向的是保存了文件所有数据的数组。只要这个切片一直保留着,垃圾回收将不能释放保存了所有数据的数组。文件一小部分有用的数据将会让所有的数据一直保存在内存中。

要解决这个问题,可以先将有用的数据先保存到一个新的切片,然后返回新的切片。

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

小礼物走一走,来简书关注我

你可能感兴趣的:(go)