Skip to content

Latest commit

 

History

History
279 lines (196 loc) · 9.19 KB

2022-10-05-Go的切片:用法和内部结构.md

File metadata and controls

279 lines (196 loc) · 9.19 KB

Go的切片:用法和内部结构

[toc]

一、介绍

Go的切片类型提供了一个方便和高效的处理类型化数据序列的方法。切片类似于其他语言的数组,但是有一些不常见的特征。这篇文章将介绍切片是什么以及如何使用。

二、数组Arrays

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

一个数组定义了特定的长度和一个元素类型。例如,类型[4]int表示一个有4个整数的数组。一个数组的大小是固定的。它的长度是它类型的一部分([4]int[5]int是不同的,不相容的类型)。数组能够用常见的方法进行索引,因此表达式s[n]读取第n个元素,从0开始。

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

数组不需要明确的被初始化;一个数组的零值是一个准备去使用的数组,元素本身是零值。

// a[2] == 0 , int类型的0值

在内存中表示[4]int 是依次存放4个整数。

Go的数组是值。数组变量表示整个数组。它并不是一个指向第一个元素的指针(和C的情况一样)。这意味着当你分配或传递一个数组值,你将创建一个内容的拷贝。(为了避免复制,您可以传递一个指向数组的指针,但那是指向数组的指针,而不是数组)。一个思考数组的方法是把它作为有序的结构,但是带有索引而不是带有字段名称:一个固定大小的合成值。

一个数组的字面量,能够被指定像这样:

b := [2]string{"aa", "bb"}

或者,你可以让编译器计算为你元素的数量。

b := [...]string{"aa", "bb"}

这两种情况,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}

当容量参数被省略的时候,默认是指定的长度。这是相同代码下一个非常简洁的版本:

s := make([]byte, 5)

一个切片的长度和容量能够被检查,使用内置的lencap函数。

len(s)
cap(s)

接下来两部分讨论长度和容量的关系。

切片的零值是nil。对于一个nil的切片,它的lencap函数都返回0。

一个切片也能通过切片现有的切片或数组来形成。切片被创建通过指定一个带有两个索引的半开的范围,中间用冒号分割。例如,表达式b[1:4]创建一个切片包含b中从1到3的元素(切片结果的索引将从0到2)。

b := []byte{'g', 'b', '1', 'a', 'n', 'g'}
// b[1:4] == []byte{'b', '1', 'a'}   和b共享相同的存储

一个切片表达式开始和结束的索引是可选的。默认分别是0 和 切片的长度。

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

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

x := [3]string{"aa", "bb", "cc"}
s := x[x]  // 引用 x 的存储的切片

四、切片的内部结构

一个切片是一个部分数组的描述符。它由一个指向数组的指针、这部分的长度、和它的容量(最大为这部分的长度)组成,

变量s,最早通过make([]byte, 5)创建,它的结构像这样:

长度是指向切片元素的数量。容量是内部数组的元素数量(从切片指针引用的位置开始)。

s = s[2:4]

切片的时候,并不会拷贝切片的数据。它创建了一个新的切片值指向原始的数组。这个操作切片的操作和操作数组索引一样高效。然后,修改重新切片的元素(不是切片本身)会修改原始切片的元素:

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 = s[:cap(s)]

一个切片不能增长超过它的容量。尝试这样做将造成一个运行时的恐慌,就像当索引超过切片或数组的边界。切片不能在零以下重新切片以访问数组中较早的元素。

五、增长切片(拷贝和添加函数)

为了让切片的容量增加,必须创建一个新的、更大的切片,并拷贝原始切片的内容到它里面。这个技术是其它语言动态数组实现在幕后工作方式。下面的例子,让s的容量翻倍,通过创建一个新的切片,t,拷贝s的内容到t中。然后分配切片t的值给s。

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

内置的复制功能使这种常见操作的循环片段变得更容易。 顾名思义,复制将数据从源切片复制到目标切片。 它返回复制的元素数。

func copy(dst, src []T) int

这个拷贝函数支持拷贝不同切片的长度(它将仅仅拷贝较小数量的元素)。除此之外,copy 能够处理源和目标切片共享相同的内部数组,从而正确处理重叠切片。

使用copy你能简化代码片段:

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

一个常用操作是添加数据到切片的结尾。这个函数添加byte元素到一个类型为byte到切片,如果需要增长切片,返回更新的切皮值。

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}

为了添加一个切片到另一个里面,使用...去扩展第二个参数成为参数列表。

a := []string{"aa", "bb"}
b := []string{"cc", "dd", "ee"}
a = append(a, b...) // 等价于 "append(a, b[0], b[1], b[2])"
// a == []string{"aa", "bb", "cc", "dd", "ee"}

因为一个切片到零值(nil)看起来像一个零长度的切片,你能声明一个切片变量,并且循环向它里面添加。

// 过滤并返回一个新的切片,仅仅持有s中满足fn()的元素
func Filter(s []int, fn func(int) bool) []int {
  var p []int
  for _, v := range s {
    if fn(v) {
      p = append(p, v)
    }
  }
  return p
}

六、一个可能的问题

正如早期提到,重新对一个切片进行切片,并不会对底层数组进行拷贝。完整的数组将包存在内存中,直到不再有引用。偶尔,这能造成问题对于持有所有数据在内存中,而只有很小一片的数据需要。

例如,这个FindDigits函数加载文件到内存中,并搜索出第一组连续的数字,把它们作为新的切片返回。

var digitRegexp = regexp.MustCompile("[1-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
}

这个函数的一个更简洁的版本能够通过使用append进行构造。

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