golang slice理解

hewolf · · 682 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

介绍

Go的切片类型提供了一种方便有效的处理类型数据序列的方法。切片类似于其他语言中的数组,但具有一些不寻常的属性。本文将介绍什么是切片并且如何使用他们

数组

切片类型是构建在Go的数组类型之上的抽象,因此为了理解切片,我们必须首先理解数组。

数组类型定义指定长度和元素类型。例如,类型[4]int表示四个整数的数组。数组的大小是固定的; 它的长度是它的类型的一部分([4]int并且[5]int是不同的,不兼容的类型)。数组可以通常的方式编入索引,因此表达式s[n]从零开始访问第n个元素。

package main

import "fmt"

func main() {
    var a [4]int
    a[0] = 1
    fmt.Println(a[0], a[2])

}
output:
1 0

数组不需要显式初始化; 数组的零值是一个现成的数组,其元素本身为零.
内存中的表示[4]int只是顺序排列的四个整数值:


Go的数组是值。数组变量表示整个数组; 它不是指向第一个数组元素的指针(如C中的情况)。这意味着当您分配或传递数组值时,您将复制其内容。(为了避免复制,你可以传递一个指向数组的指针,但那是一个指向数组的指针,而不是一个数组。)一种思考数组的方法是作为一种结构,但有索引而不是命名字段:一个固定的-size复合值。
数组可以这么定义:

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

你也可以这么定义:

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

两种定义方式变量都是[2]string类型

slices

数组有它们的位置,但它们有点不灵活,所以你不会在Go代码中经常看到它们。然而,切片无处不在。它们以阵列为基础,提供强大的功能和便利性。

切片的类型规范是[]T,切片T元素的类型。与数组类型不同,切片类型没有指定的长度。

切片文字声明就像数组文字一样,除了省略元素数:
可以使用调用的内置函数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}

省略capacity参数时,默认为指定的长度。这是相同代码的更简洁版本:

s:= make([] byte,5)

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

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

切片长度和容量的关系

还可以通过“切片”现有切片或阵列来形成切片。通过指定半开放范围来完成切片,其中两个索引用冒号分隔。例如,表达式b[1:4]创建一个包含元素1到3的b切片(结果切片的索引将为0到2)。

b:= [] byte {'g','o','l','a','n','g'}
// b [1:4] == [] byte {'o','l','a'},与b共享相同的存储空间

切片表达式的开始和结束索引是可选的; 它们分别默认为零和切片长度:

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

这也是给定数组创建切片的语法:

x:= [3] string {“Лайка”,“Белка”,“Стрелка”}
s:= x [:] //引用x存储的切片

切片内部

切片是数组段的描述符。它由指向数组的指针,段的长度及其容量(段的最大长度)组成
[站外图片上传中...(image-2fae1-1563440373053)]
我们s之前创建的变量的make([]byte, 5)结构如下:
[站外图片上传中...(image-2218ae-1563440373054)]
长度是切片引用的元素数。容量是底层数组中元素的数量(从切片指针引用的元素开始)。在我们通过接下来的几个例子时,将明确区分长度和容量。

在切片时s,观察切片数据结构的变化及其与底层数组的关系:

s = s [2:4]

[站外图片上传中...(image-6f6534-1563440373054)]
切片不会复制切片的数据。它创建一个指向原始数组的新切片值。这使切片操作与操作数组索引一样高效。因此,修改重新切片的元素(而不是切片本身)会修改原始切片的元素:

    d := []byte{'r', 'o', 'a', 'd'}
    e := d[2:]
    fmt.Println(e)
    fmt.Println(d)
    e[1] = 'm'
    fmt.Println(e)
    fmt.Println(d)
output:
[97 100]
[114 111 97 100]
[97 109]
[114 111 97 109]

切片不能超出其容量。尝试这样做会导致运行时出现混乱,就像在切片或数组的边界之外进行索引一样。类似地,切片不能在零以下重新切片以访问数组中的早期元素

生长切片(复制和追加功能)

要增加切片的容量,必须创建一个新的更大的切片并将原始切片的内容复制到切片中

        s := make([]byte, 1)
    fmt.Println(s, len(s), cap(s))
    t := make([]byte, len(s), (cap(s)+1)*2)
    //for i := range s {
    //  t[i] = s[i]
    //}
    copy(t, s)
    fmt.Println(t, len(t), cap(t))
    output:
    [0] 1 1
    [0] 1 4

扩容后,使用range赋值还是copy函数两种方式是对等的

func copy(dst,src [] T)int

当然copy方法简化了赋值的步骤
一个常用的操作是动态增加容量,举个例子:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

func main() {
    a := []byte{1, 2}
    fmt.Println(a)
    a = AppendByte(a, 3, 4, 6)
    fmt.Println(a)
}
output:
[1 2]
[1 2 3 4 6]

上面的例子显示如果容量不够,以两倍容量增加,当然算法都是根据需求自定义

不过大多数情况不需要太复杂操作,可以通过append函数将新元素加入到slice尾部,如果slice容量不够,系统会动态添加容量

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

举例如下:

a := make([]int, 1)
fmt.Println(a, len(a), cap(a))
a = append(a, 1, 2, 3)
fmt.Println(a, len(a), cap(a))
output:
[0] 1 1
[0 1 2 3] 4 4

如果想通过append添加另一个slice,可以如下操作:

 b := []string{"John", "Paul"}
fmt.Println(b, len(b), cap(b))
c := []string{"George", "Ringo", "Pete"}
b = append(b, c...)
fmt.Println(b, len(b), cap(b))
var d []string
b = append(b, d...)
fmt.Println(b, len(b), cap(b), d, d == nil)
output:
[John Paul] 2 2
[John Paul George Ringo Pete] 5 5
[John Paul George Ringo Pete] 5 5 [] true

nil slice也可以进行append

一个可能的陷阱

如前所述,重新切片切片不会复制底层数组。完整数组将保留在内存中,直到不再引用它为止。偶尔这会导致程序在只需要一小部分数据时将所有数据保存在内存中。

例如,此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 
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:hewolf

查看原文:golang slice理解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

682 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传