GoLang学习 -- array(数组)和slice(切片)

在使用golang过程当中,经常会用到数组类型。可在查看golang官方文档中发现,在golang语言当中,除了存在数组类型之外,还存在有切片类型。这个切片类型在其他语言都没有出现过,那么这个切片类型到底是一种什么类型呢?切片和数组又有什么区别? 下面就谈谈在golang中数组和切片的故事。

首先我们看一下数组:
golang中的数组是一种由固定长度和固定对象类型所组成的数据类型。例如下面:

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

a是一个拥有4个int类型元素的数组。当a一旦被声明之后,元素个数就被固定了下来,在a这个变量的生命周期之内,元素个数不会发生变化。而此时a的类型就是[4]int,如果同时存在一个b变量,为[5]int。即便两个变量仅仅相差一个元素,那么在内存中也占据着完全不同的地址分配单元,a和b就是两个完全不同的数据类型。

在golang中,数组一旦被定义了,那么其内部的元素就完成了初始化。例如此时访问a[1],就是0.因此int的初始化值就是0.

a[1]==0

a的内存分布此时是下面的情况:
这里写图片描述

在golang当中,一个数组就是一个数据实体对象。在golang当使用a时,就代表再使用a这个数组。而在C中,当使用a时,则代表着这个数组第一个元素的指针。这点请从C语系转来的同学注意。

为什么要提到这点呢?因为这点对于后面的讲解来说非常重要。正因为a代表的是整个数组对象,所以对a的增删修改其实都是数组对象拷贝的操作。换言之,golang中的数组都是值拷贝而不是引用拷贝。如果你想对数组本身进行操作,那么就操作指向这个数组对象的指针,也就是将值拷贝改为引用拷贝。

在golang当中,虽然数组是一个内置的数据类型,但”僵硬”不好用。因此golang推荐使用切片,切片相对于数组而言,显得更加的优美和灵活。

golang当中切片的定义是: []T. T表示的切片元素类型。与数组不同的时,切片在声明时不需要指定元素个数(长度)。切片有两种声明方式:

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

乍一看,和数组的声明很相似。但和数组声明不同的时,切片声明不需要指定元素个数。相比之下,golang更为推崇另外一种声明方式:使用内置的make函数。

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

使用make函数时,需要三个参数,分别是:T(切片元素类型),len(切片元素个数)和cap(切片容量)。典型的用法如下:

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

而一般使用时,经常省略容量,而变为:

s := make([]byte, 5)

当使用这种省略方式时,容量就默认为元素长度。

当切片声明之后,可以通过内置的len函数和cap函数,分别获取切片的元素个数和容量,如下所示:

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

读者也许会问到,元素个数和容量个数之间有什么关系呢? 这个问题,我们会在下面讲到,请耐心往下看。

一个已经声明好的切片,是可以再次被切片化。如:

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

b是一个切片,可以通过[n:m]左开右闭的方式基于这个切片再生成一个切片。上图中,b[1:4]就生成了一个新切片,切片元素范围为1,2,3,不包括4.

[n:m]的方式还存在多种简写方式,如下图所示:

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

这些都是基于切片而生成的切片,golang中允许基于数组生成一个切片,如下:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x

当切片生成之后,就可以向使用数组一样使用切片了。例如通过下标访问某个元素,也可以通过下标修改某个元素。

有人会问,这样看来,切片也就是一个不定个数的数组而已吧?没有什么区别呀?

下面,我们就来看看切片和数组究竟有什么不同。

切片从内部数据结构来说,就和数组不同。虽然切片使用和数组几乎没有区别,但内部结构却相差万里,下图是切片的数据结构:
GoLang学习 -- array(数组)和slice(切片)_第1张图片

切片结构中有三个属性,分别是指向首元素的指针,保存元素个数的整形数值和保存容量个数的整形数值。以我们最开始创建的s 切片为例,其内部是这样的:
GoLang学习 -- array(数组)和slice(切片)_第2张图片

指针指向了一个数组的首元素,len是此切片中的元素个数,这里是5.而cap指的是从数组指向的首元素开始到数组最后一个元素为止,这里也是5.

如果我们对s进行二次切片,如下:

s = s[2:4]

那么情况就会发生变化,如下所示:
GoLang学习 -- array(数组)和slice(切片)_第3张图片

len=2好理解,因为新切片只包含原切片的第三个和第四个元素,因此元素个数为2.但cap=3又如何理解呢? 上面我们提到cap指的是指向数组的首元素到数组最后一个元素的个数。现在新切片首元素是从原数组第三个元素开始的,原数组一共有5个元素,因此cap就是3,4,5这三个元素个数,所以cap=3. 最后的指针就非常容易理解了吧,因为这个切片是从原数组的第三个元素开始的,所以指针理所当然的就指向了第三个元素。

依次类推,如果s=s[1:3]后。那么指针指向原数组的第二个元素,len=2,cap=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'}

通过上面对len和cap的讲解,大家应该明白len不可能大于cap。如果len比cap还大,那么就会触发运行时异常。可在实际编码过程中,如何对切片进行扩容呢?这将引出下一个话题:如何动态操作切片?

动态操作切片,依赖两个内置函数copy和append。在其它低级语言中,尤其是C语言。再操作数组之时,经常会判断当前数组是否还有剩余空间,如果没有剩余空间就要新生成一个新的数组,然后将旧数组拷贝给新数组。代码类似于下面:


声明t数组
for i := range s {
        t[i] = s[i]
}
s = t

在golang当中,读者也可以这么操作。但是golang毕竟是一门高级语言,它内部已经内置了copy函数,可以把循环赋值部分节省点。使用copy函数的代码如下:

//func copy(dst, src []T) int
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) { // 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
}

这样以后我们再向切片中添加数据时,就不用考虑溢出的情况了,如下:

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

Golang因为向切片添加数据是一个经常使用的功能,所以它提供了append函数来替我们完成了上述的工作。

下面是append函数的声明:

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

当使用append函数时,append函数会判断目的切片是否具有剩余空间,如果没有剩余空间,则会自动扩充两倍空间。下面是使用append函数的几个例子:

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

append函数不只能添加元素,还能将添加切片,如下:

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

下面是有一个实际代码,通过提供的判断函数,只添加需要的元素,如下:

/ Filter returns a new slice holding only
// the elements of s that satisfy f()
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
}

当在golang中使用切片时,golang会预先把切片所指向的数组都加载到内存中。因此可能会出现一种情况:我们仅仅只需要数组中的一小部分数据,但golang却加载了所有的数据。例如下面的例子:

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
}

这样,最终内存中只会存在c所有的少量数据。而预先加载的文件数据则会被垃圾回收器所释放掉。

好了,上面就是golang语言中,数组和切片的异同之处。 若讲解有不对的地方,欢迎留言讨论。

你可能感兴趣的:(google,code,cloud,golang)