微信号:go-programming-lang

介绍:重点介绍go语言的高级功能,比如interface、 reflection、goroutine、concurrency、memory model、testing、functional、toolchains等.

Go slices的用法和内部机制

2016-07-23 11:59 Oscar 译

简介

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。使用lencap函数时,返回值都是0

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

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

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

起始和结束的下标都是可选的,默认值分别是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即切片中元素的个数

容量(capacityslice基于的数组的元素个数(从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

t := make([]byte, len(s), (cap(s)+1)*2) // +1 in case cap(s) == 0
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...) // equivalent to "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包含slicearrary的深入探讨,Go语言规格定义了slice和相关的辅助函数。

 

原作者:Andrew Gerrand,翻译:Oscar

相关链接:

原文链接:https://blog.golang.org/go-slices-usage-and-internals

Effective Gohttp://golang.org/doc/effective_go.html

Effective Go sliceshttp://golang.org/doc/effective_go.html#slices

Go 语言规格:http://golang.org/doc/go_spec.html

Go 语言规格 slices http://golang.org/doc/go_spec.html#Slice_types


 
深入Go语言 更多文章 Go语言并发模型:使用 select Go语言并发模型:使用 context Go语言并发模型:以并行处理MD5为例 Go语言并发模型:像Unix Pipe那样使用channel Go语言反射三定律
猜您喜欢 WEB前端MVC架构变形记 搜索广告和展示广告的数据和趋势 必备的 C++ 资源大全:重温 11 篇热文 ThinkPHP祝大家春节快乐,马年如意! 【干货】MySQL5.6配置同步复制的新方法以及常见问题的解决方法