《Go语言四十二章经》第十二章 切片(slice)

作者:李骁

12.1 切片(slice)

切片(slice) 是对底层数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(和数组不一样)。切片提供对该数组中编号的元素序列的访问。 切片类型表示其元素类型的所有数组切片的集合。未初始化切片的值为nil。

与数组一样,切片是可索引的并且具有长度。切片s的长度可以通过内置函数len() 获取;与数组不同,切片的长度可能在执行期间发生变化。元素可以通过整数索引0到len(s)-1来寻址。我们可以把切片看成是一个长度可变的数组。

切片提供了计算容量的函数 cap() ,可以测量切片最大长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说,这个不等式永远成立:0 <= len(s) <= cap(s)。

一旦初始化,切片始终与保存其元素的基础数组相关联。因此,切片会和与其拥有同一基础数组的其他切片共享存储;相比之下,不同的数组总是代表不同的存储。

切片下面的数组可以延伸超过切片的末端。容量是切片长度与切片之外的数组长度的总和。

使用内置函数make()可以给切片初始化,该函数指定切片类型和指定长度和可选容量的参数。

切片与数组相比较:

优点

因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。

声明切片的格式是: var identifier []type(不需要说明长度)。一个切片在未初始化之前默认为 nil,长度为 0。

切片的初始化格式是:

var slice1 []type = arr1[start:end]

这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。

切片也可以用类似数组的方式初始化:

var x = []int{2, 3, 5, 7, 11}

这样就创建了一个长度为 5 的数组并且创建了一个相关切片。

当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片,同时创建好相关数组:

var slice1 []type = make([]type, len,cap)

也可以简写为 slice1 := make([]type, len),这里 len 是数组的长度并且也是 slice 的初始长度。cap是容量,其中 cap 是可选参数。

v := make([]int, 10, 50)

这样分配一个有 50 个 int 值的数组,并且创建了一个长度为 10,容量为 50 的 切片 v,该切片指向数组的前 10 个元素。

以上我们列举了三种切片初始化方式,这三种方式都比较常用。

如果从数组或者切片中生成一个新的切片,我们可以使用下面的表达式:

a[low : high : max] max-low的结果表示容量,high-low的结果表示长度。

a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]

这里t的容量(capacity)是5-1=4 ,长度是2。

如果切片取值时索引值大于长度会导致panic错误发生,即使容量远远大于长度也没有用,如下面代码所示:

package main

import "fmt"

func main() {
    sli := make([]int, 5, 10)
    fmt.Printf("切片sli长度和容量:%d, %d\n", len(sli), cap(sli))
    fmt.Println(sli)
    newsli := sli[:cap(sli)]
    fmt.Println(newsli)

    var x = []int{2, 3, 5, 7, 11}
    fmt.Printf("切片x长度和容量:%d, %d\n", len(x), cap(x))

    a := [5]int{1, 2, 3, 4, 5}
    t := a[1:3:5] // a[low : high : max]  max-low的结果表示容量  high-low为长度
    fmt.Printf("切片t长度和容量:%d, %d\n", len(t), cap(t))

    // fmt.Println(t[2]) // panic ,索引不能超过切片的长度
}

程序输出:
切片sli长度和容量:5, 10
[0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]
切片x长度和容量:5, 5
切片t长度和容量:2, 4

12.2 切片重组(reslice)

slice1 := make([]type, start_length, capacity)

通过改变切片长度得到新切片的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。

当我们在一个slice基础上重新划分一个slice时,新的slice会继续引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用。

package main

import "fmt"

func get() []byte {  
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &raw[0]) // 显示: 10000 10000 数组首字节地址
    return raw[:3]  // 10000个字节实际只需要引用3个,其他空间浪费
}

func main() {  
    data := get()
    fmt.Println(len(data), cap(data), &data[0]) // 显示: 3 10000 数组首字节地址
}

为了避免这个陷阱,我们需要从临时的slice中使用内置函数copy(),拷贝数据(而不是重新划分slice)到新切片。

package main

import "fmt"

func get() []byte {
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &raw[0]) // 显示: 10000 10000 数组首字节地址
    res := make([]byte, 3)
    copy(res, raw[:3]) // 利用copy 函数复制,raw 可被GC释放
    return res
}

func main() {
    data := get()
    fmt.Println(len(data), cap(data), &data[0]) // 显示: 3 3 数组首字节地址
}

程序输出:
10000 10000 0xc000086000
3 3 0xc000050098

append()内置函数:

func append(s S, x ...T) S  // T是S元素类型

Append()函数将 0 个或多个具有相同类型 S 的元素追加到切片s后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。

因此,append()函数返回的切片可能已经指向一个不同的相关数组了。append()函数总是返回成功,除非系统内存耗尽了。

s0 := []int{0, 0}
s1 := append(s0, 2)                // append 单个元素     s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)          // append 多个元素    s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)            // append 一个切片     s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)   // append 切片片段    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

append()函数操作如果导致分配新的切片来保证已有切片元素和新增元素的存储,也就是返回的切片可能已经指向一个不同的相关数组了,那么新的slice已经和原来slice没有任何关系,即使修改了数据也不会同步。

append()函数操作后,有没有生成新的slice需要看原有slice的容量是否足够。

12.3 陈旧的切片(Stale Slices)

多个slice可以引用同一个底层数组。在某些情况下,在一个slice中添加新的数据,在原有数组无法保持更多新的数据时,将导致分配一个新的数组。而现在其他的slice还指向老的数组(和老的数据)。

上一节我们也说了:append()函数操作后,有没有生成新的slice需要看原有slice的容量是否足够。

下面,我们看看这个过程是怎么产生的:

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}
    fmt.Println(len(s1), cap(s1), s1) // 输出 3 3 [1 2 3]
    s2 := s1[1:]
    fmt.Println(len(s2), cap(s2), s2) // 输出 2 2 [2 3]
    for i := range s2 {
        s2[i] += 20
    }
    // s2的修改会影响到数组数据,s1输出新数据
    fmt.Println(s1) // 输出 [1 22 23]
    fmt.Println(s2) // 输出 [22 23]

    s2 = append(s2, 4) // append  s2容量为2,这个操作导致了slice s2扩容,会生成新的底层数组。

    for i := range s2 {
        s2[i] += 10
    }
    // s1 的数据现在是老数据,而s2扩容了,复制数据到了新数组,他们的底层数组已经不是同一个了。
    fmt.Println(len(s1), cap(s1), s1) // 输出3 3 [1 22 23]
    fmt.Println(len(s2), cap(s2), s2) // 输出3 4 [32 33 14]
}


程序输出:
3 3 [1 2 3]
2 2 [2 3]
[1 22 23]
[22 23]
3 3 [1 22 23]
3 4 [32 33 14]

本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42
本书《Go语言四十二章经》内容在简书同步地址: https://www.jianshu.com/nb/29056963

虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。
联系邮箱:roteman@163.com

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

推荐阅读更多精彩内容

  • 数组Go语言中的数组是定长的同一类型数据的集合,数组索引是从0开始的。数组有以下几种创建方式 以下是一些特殊数组 ...
    小杰的快乐时光阅读 1,674评论 0 0
  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
    Tuberose阅读 18,398评论 1 46
  • 一、Go语言中切片类型出现的原因 切片是一种数据类型,这种数据类型便于使用和管理数据集合。创建一个100万个int...
    码墨阅读 1,776评论 0 1
  • 昨天是考研成绩陆续出来的日子,整好50天。个中滋味,实难说清,无数次的焦灼,假设,心理建设……却终究在刷新...
    姜酱犟君阅读 259评论 1 1
  • @风风光光刚刚
    阿宽_7d4c阅读 333评论 0 0