您的位置 首页 php

Go语言 array/slice的用法和内部机制

简介

Go语言的slice(切片,后面统一使用slice)类型为处理一组同类型数据提供了便捷的方法。它与其它编程语言的“数组”有些类似,但相对而言包含了更多的特性。通过这篇文章,我们来看一看slice到底是什么,如何去使用它。

Arrays(数组)

Slice是建立在arrary(数组)类型上的一种抽象,要理解slice,我们必须先了解一下数组。

array的定义中包含一个数组长度和元素类型字段。举个例子,类型[4]int 表示一个有四个整型数的数组。数组的类型是固定的,它的长度也是类型的一部分,因此 [4]int 和 [5]int 是两个完全不同的数据类型。我们可以使用下标对数组进行检索,表达式s[n] 即数组的第n个元素(从0开始)。

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

使用者不需要对数组进行显式初始化,一个零值的数组的所有元素默认被初始化为0:

 // a[2] == 0, the zero value of the int type  

类型 [4]int 的一个数组在内存中表现为四个连续存放的整型数:

Go语言中,数组是“值”,一个数组变量代表整个数组;注意,与C语言不同,它不是指向数组首元素的指针。这意味着,当你把一个数组变量进行传递或赋值时,你会得到它的一份拷贝。为了避免拷贝,可以传递指向该数组的指针。你可以把数组当成一种结构体(struct),只是通过下标而不是字段名获取元素,或者当成一个固定大小的组合值。

我们可以使用下面这种方式定义一个数组:

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

不指定元素个数也可以,编译器会自动计算:

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

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

Slices(切片)

数组有一些应用场景,但是不太灵活,所以在go语言的代码中不经常出现。但是切片可以随处可见。切片建立在数组之上,但是功能和易用上都更胜一筹。

切片的类型规格是 []T,这里 T 是元素的类型。不像数组,切片没有特定的长度。

切片变量的定义和数组有些类似,但是不用定义长度:

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

切片也可以使用make函数进行创建,make的语言规格如下:

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

这里 T 表示将被创建切片的元素类型。Make函数接受三个参数: 类型 长度(length) 容量(capacity) 。第三个参数是可选的,如果不设置,则与“长度”一致。被调用时,make分配一个数组,然后返回一个指向该数组的slice。

 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  

下两个环节我们会讨论长度和容量的关系。

切片的零值是 nil。使用len和cap函数时,返回值都是0。

还有一种创建切片的方式:slicing(切割)。切割操作时通过一个半开的域来定义的,语法上表现为使用冒号分开的两个下标。举个例子,表达式 b[1:4] 会创建一个包含b第1、2、3位置三个元素的切片,新切片的长度是3:

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

起始和结束的下标都是可选的,默认值分别是0和原始切片的长度:

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

基于数组创建切片的语法类似:

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

slice的内部机制

切片是数组片段的描述符,它包含一个指向数组的指针、片段的长度、容量(片段的最大长度)。

之前通过make([]byte,5)创建的变量s的内存结构如下:

长度(length) 即切片中元素的个数

容量(capacity) 是slice基于的数组的元素个数(从slice指针指向的第一个元素开始计算)。

后面我们还会讲几个例子,长度和容量的差别会越来越清晰。

我们对 s 进行切割,观察数据结构的变化,以及与底层数组关系的变化。

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

增长切片(通过copy和append函数)

如果要增加一个切片变量的长度,你必须创建一个新的、更大切片变量,然后将原切片的内容拷贝过去。这项技术时从其它语言的动态数组学来的。下一个例子中,我们将通过创建一个新切片t 来将原切片 s 的容量扩大一倍,然后将 s 的内容拷贝到 t,最后将 t 赋值给 s。

 // +1 以免 cap(s) == 0
t := make([]byte, len(s), (cap(s)+1)*2) 
for i := range s {
  t[i] = s[i]                
}
s = t  

遍历赋值的操作可以使用内置的copy函数实现。这个函数正如其名,将数据从一个切片拷贝到另一个切片,返回拷贝元素的数量。

 func copy(dst, src []T) int  

copy 函数支持在不同长度的切片之间拷贝数据(以元素个数较少的为准)。另外,如果两个切片共享一个底层数组,即便两个切片的数据存在重叠部分,copy 函数也能正确处理。

使用 copy 函数,上面的代码可以简化为:

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

切片的一个常用操作是向末尾添加数据。AppendByte 函数支持向byte切片添加byte元素,必要时自动增长切片,返回更新过的切片。

 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
}  

你可以像下面这样使用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{"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)和零长度表现是一致的,你可以声明一个切片,然后在一个循环里对它赋值:

 // 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
}  

一个可能的“坑”

之前提到,重新切割不会拷贝底层的数组,所以整个数组会一致保留在内存中,知道没有变量去引用它。在极少数情况下,这可能会导致程序把一大整块数据都保留在内存中,而只用到极少的一部分。

举个例子,FindDigits函数加载一个文件到内存中,查询一组连续的数字,并作为一个新的slice返回。

 var digitRegexp = regexp.MustCompile("[0-9]+")

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

这段代码表现很正常,但是返回了的 []byte 指向的数组包含整个文件的内容。由于切片指向原始数组,只要这个切片存在,gc就不会释放底层数组。极少有用的数据却把整个文件的内容卡在内存里。

为了修正这个问题,你可以把有用的数据存放到一个新的切片中,然后返回它:

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

这个函数更准确的版本可以借助于append实现,这里留给读者去思考。

扩展阅读

Effective Go包含slice和arrary的深入探讨,Go语言规格定义了slice和相关的辅助函数。

原作者:Andrew Gerrand,翻译:赵帅虎

相关链接:

原文链接:

Effective Go:

Effective Go slices:#slices

Go 语言规格:

Go 语言规格 slices: #Slice_types

文章来源:智云一二三科技

文章标题:Go语言 array/slice的用法和内部机制

文章地址:https://www.zhihuclub.com/77248.shtml

关于作者: 智云科技

热门文章

网站地图