Go语言切片深层解析

一、Go语言中切片类型出现的原因

切片是一种数据类型,这种数据类型便于使用和管理数据集合。
创建一个100万个int类型元素的数组,并将它传递给函数,将会发生什么?

var array [le6]int
   foo(array)
  fun foo(array [le6]int){
   ...
}

在64位架构上,100个int类型的数组需要800万字节,即8M的内存。由于Go语言只有值传递,每次调用函数都需要在栈上分配8M的空间并将数组内容复制进去,这不仅浪费内存而且复制还消耗CPU,当数组较大时复制速度较慢也影响程序使用体验。因此可以只需要传入数组的地址,地址在64为系统上只需要消耗8字节,这样可以更好的利用内存和提升性能,但是由于传入的指针,当函数内部修改了指针的指向内容数组也会发生改变,因此设计了切片来处理数这类数组的共享问题。

二、切片深层解析

面试题

func main() {
    s := []int{1, 2, 3}                          
    ss := s[1:]                                        
    ss = append(ss, 4)

    for _, v := range ss {
        v += 10
    }
    for i := range ss {
        ss[i] += 10
    }
    fmt.Println(s)
}

上面那道面试题是对于切片的考察,首先我们需要明白切片的结构。
slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定

struct    Slice
{    // must not move anything
    byte*    array;        // actual data
    uintgo    len;        // number of elements
    uintgo    cap;        // allocated number of elements
};

这个结构有3个字段,第一个字段表示array的指针,就是真实数据的指针(这个一定要注意),第二个是表示slice的长度,第三个是表示slice的容量,特别需要注意的是:

slice的长度和容量都不是指针

但我们使用 make([]byte, 5) 创建一个切片变量 s 时,它内部的存储的结构如下:


长度是切片引用的元素数目,容量是底层数组的元素数目(从切片指针开始)。
我们对 s 进行切片,观察切片的数据结构和它引用的底层数组:

s=s[2:4]

切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。

前面创建的切片 s 长度小于它的容量。我们可以增长切片的长度为它的容量:

s = s[:cap(s)]

切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。

三、切片的创建与使用

刚开始使用切片类型的时候很多人很疑惑这样一个问题:

fun main(){
  slice :=[]int{1,2,3}
  changeSlice(slice)
  fmt.Println("slice:",slice)
}

func changeSlice(s []int){
  s=append(s,10)
}

这个问题的输出是: 1 2 3
为什么10没有append到切片里面了?
因为通过函数传递slice作为参数的时候,形参拷贝实参的slice结构,但是由于 array部分是指针因此形参与实参共享底层数组,但是len和cap是会发生拷贝,当形参s进行append的时候,len会发生变化,但是实参的len没变,当输出实参slice的值时,只根据它现在的len进行输出,因此输出1 2 3。同理:

slice:=[]int{1,2,3}
s=slice[0:2]
s.append(s,10)

虽然slice与s同用底层数组,但是slice与s的len不相同,因此输出的slice值与s值也不相同。

创建和初始化切片

1、通过数组创建初始化slice

str  :=[5]string{"red","blue","Green","Yellow","Pink"}
slice :=str[:]

使用数组初始化创建切片后,切片会与切片共享底层数组,当修改切片或者数组的值时会相互影响,直到如果对切片添加数据超出cap限制,则会为新切片对象重新分配数组。
2、通过make创建并初始化切片
通过make创建切片需要指定至少出入一个参数,指定切片的长度,如果只指定切片的长度,那么切片的容量与长度相等。也可以分别指定长度与容量,且容量要大于等于长度。
通过make创建的切片会自动初始化slice长度范围内值为0。
面试题

func main() {
    s := make([]int, 5)
    s = append(s, 1, 2, 3)
    fmt.Println(s)
}
结果为: 0 0 0 0 0 1 2 3

3、通过切片字面量创建切片

str :=[]string{"red","blue","Green","Yellow","Pink"}

切片的长度与容量会基于初始化提供的元素的个数确定。
使用切片字面量时,可以设置长度和容量,slice:=[]string{99:""},创建长度与容量都是100个元素的切片。
如果在[]运算符里面指定一个值,那么创建是数组而不是切片。

nil与空切片
var slice []int
b:=[]int{}
println(a == nil,b==nil)
结果 true false

前者仅仅定义了一个[]int类型的变量,并未执行初始化操作,而后者初始化表达式完成了全部的创建。
但需要描述一个不存在的切片的时候nil很好使用,常用在函数返回。

空切片在底层数组包含0个元素,没有分配任何空间。表示空集合的时候空切片很好使用。

切片的增长

相对于数组而言,实用切片的一个好处就是可以按需增加切片的容量。Go语言内置的append函数会处理增加长度时所有的操作细节。
使用append时,需要一个被操作的切片和一个要追加的值。函数append调用返回时,会返回一个包含修改结果的新切片。函数append总会增加新切片的长度,而容量有可能会发生改变,也可能不会改变,这取决于被操作切片的可用容量。
如果切片底层数组没有足够的可用容量,append函数会创建一个新的底层数组,将被引用的现有值复制到新数组里,再追加新的值。

slice:=[]int{1,2,3,4}
newSlice :=append(slice,50)

append后,newSlice和slice使用不同的底层数组。
函数append会智能地处理底层数组的容量增长。在切片的容量小于1000个元素时,总是会成倍的增加容量。一旦元素个数超过1000,容量的增长因子会设为1.25。随着增长算法的改变,增长因子有可能会发生改变。

四、可能的“陷阱”

切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。 有时候可能会因为一个小的内存引用导致保存所有的数据。

例如, FindDigits 函数加载整个文件到内存,然后搜索第一个连续的数字,最后结果以切片方式返回

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
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 线性结构是计算机最常用的数据结构之一。无论是数组(arrary)还是链表(list),在编程中不可或缺。golan...
    _二少爷阅读 6,596评论 5 13
  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
    Tuberose阅读 18,398评论 1 46
  • 元正同学今天破天荒的第一次起了个大早,六点钟不到的样子就在被窝里嘚啵了。“爸爸呢?他去见朋友啦?”“他去接姐...
    元正妈妈阅读 194评论 0 0
  • 认真二字是提命于自己的 无论何时何境,认真活着 一壶水一碗粥 一弯腰一席净地 痛与痒是偷缝的阴霾,估计最怕认真二字
    异常值阅读 130评论 0 0
  • 苦蝉孑立老树喧啾, 歌诀里揶揄着离愁 灼洗大地的骄阳不语, 杨柳涤去沧桑,舒展枝头 顽童仍旧似风竞逐 控诉溪水掠夺...
    Mr远远阅读 183评论 0 0