Go语言中的切片类型

图文无关

本文翻译自Andrew Gerrand的博文 https://blog.golang.org/go-slices-usage-and-internals

前言

Go语言中提供了的切片类型,方便使用者处理类型数据序列。
切片有点像其他语言中的数组,并且提供了一些额外的属性。

数组

Go语言自带了数组类型,而切片类型是基于数组类型的抽象。因此,要理解切片类型,我们必须首先理解数组。
定义一个数组时,需要指定数组长度和数组中元素的类型,比如说 [4]int定义了长度为4的数组,其中的元素类型为int。一个数组的长度是固定的;长度是数组类型的一部分([4]int[5]int就是两个不同的类型)。数组以通常的方式进行索引,所以表达式s[n]能访问到数组s的第n个元素(从0开始)。

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

在没有显式初始化时,数组默认会将元素初始化为0。

// a[2] == 0

在内存中,[4]int表示为顺序排列的4个整数值

内存中的 [4]int

Go语言中的数组是一个值。数组变量表示整个数组,而不是指向数组第一个元素的指针(就像C语言那样)。这就意味着,将一个数组当作一个参数传递时,会完全拷贝数组中的内容(如果不想完全拷贝数组,可以传一个指向数组的指针)。
可以把数组当成这样一种结构,它具有索引,有着固定的大小,可以用来存储不同类型的元素。

一个字符串数组可以这样定义

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

或者让编译器来确定数组的长度

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

上面的两个例子中,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}

如果没有传入cap参数,它的默认值是传入的长度。这是上面代码的一个简洁版本。

s := make([]byte, 5)

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

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

下面两个章节将讨论长度和容量的关系。
切片的零值为nil。对一个值为nil的切片来说,lencap会返回0。

可以通过“切”一个数组或者是切片,来生成新的切片。这个过程通过指定两个索引的半开范围来完成,两个索引之间用冒号隔开。举个例子,b[1:4]会返回一个新的切片,包含的元素为b中的第1到第3的元素

b ;= []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}  和b中的元素占用同一块内存

起始和结束索引是可选的,其默认值分别为0和切片的长度

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

基于数组创建切片语法与上面的类似。

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:]     // s为指向x的引用

探寻切片内部

切片是数组段的描述符。它包含了一个指向数组的指针,数据段的长度和容量。

切片结构

通过s := make([]byte, 5)方式声明的切片结构如下

s结构

长度是切片指向内容中元素的个数。容量是底层数组中的元素个数(从切片指向的元素开始计数)。长度和容量的区别会在下面的例子中解释。

s进行切片,观察下面切片和数组的关系

s = s[2:4]
切片和数组

切片操作并不会拷贝s中的数据,而是创建一个新的切片指向原来的数组,这让切片操作就像操作数组索引一样高效。因此,对切片的元素进行修改,会修改原始切片的元素。

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[:cap(s)]
切片后结果

切片的长度不能大于其容量。这样做会导致一个runtime panic,就像对切片或者数组进行越界访问一样。

增加切片容量

要增加切片的容量,必须新建一个容量更大的切片,然后将之前的切片的数据拷贝到新的切片中。这也是其他语言实现动态数组的方式。下面的例子,新建一个容量是s两倍的切片t,然后将s的数据拷贝到t中,最后将t赋值给s:

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

使用内置的copy函数可以简化上面的代码。顾名思义,copy将数据从一个切片拷贝到另一个切片,并返回拷贝元素的数量。
语法如下:

func copy(dst, src []T) int

函数copy 支持两个不同长度切片之间的拷贝。另外,copy可以处理源和目的切片指向相同底层数组的情况,正确处理重叠的切片。

简化上面的代码

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

一个常见的操作是在切片的末尾添加一个元素。下面的函数在一个切片的末尾增加一个元素,在容量不够的情况下增加切片的容量,并且返回更新后的切片

func AppendByte(slice []byte, data ...type) []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函数就派上用场了。它的语法如下

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

函数appendx添加到s末尾,如果需要就扩展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)和长度为0的切片相似,可以声明一个切片变量,然后在循环中在其末尾添加元素。

// 通过fn筛选出s中的元素
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函数会将一个文件中的内容保存在内存中,搜索第一组连续数字,并将它们作为新的切片返回。

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
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 切片(slice)是 Golang 中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合。切片是围绕动态...
    小孩真笨阅读 1,052评论 0 1
  • 一、Go语言中切片类型出现的原因 切片是一种数据类型,这种数据类型便于使用和管理数据集合。创建一个100万个int...
    码墨阅读 1,783评论 0 1
  • 最近面试较多,但其实很多内容自己也不太会,所以有了自问自答的环节。a.什么是BFC浮动元素和绝对定位元素,非块级盒...
    等花开_8e16阅读 385评论 0 0
  • 你有没有想过我也很好 只是你没发现
    wsno阅读 274评论 0 0
  • 今天关于身上的红点,有了新的解释,那就是被虫子的体液毒到了。因为根据妈妈的理论,过敏的话应该全身长红点。
    木卫33阅读 111评论 0 0