Go语言中的切片类型

作者: 大蟒传奇 | 来源:发表于2016-12-18 16:02 被阅读549次
    图文无关

    本文翻译自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的引用
    

    探寻切片内部

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

    切片结构

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

    s结构

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

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

    s = s[2: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)]
    
    切片后结果

    切片的长度不能大于其容量。这样做会导致一个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
    

    函数appendx添加到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
    }
    

    相关文章

      网友评论

      • 洛卡思:楼主,我有一点不太明白,还请您多做一下解释!为什么最后解决这个坑的办法是用copy函数,这个复制后的C切片不会去指向原来的原来的数组吗?
        大蟒传奇:hihi,是的。
        示例中的优化措施的场景是,“对切片进行切片会让原切片无法被回收(即使已经不再需要全部的数据)“,实际是是对资源利用的一个优化

      本文标题:Go语言中的切片类型

      本文链接:https://www.haomeiwen.com/subject/jajamttx.html