数组切片和append函数

Slice模型

切片(Slice)可以看作是对数组的一种包装形式.也就是说,切片的实现还是数组.
让我们从创建将起:首先我们声明一个切片

[]string{"test1","test2","test3"}

此时go会创建一个长度为3的字符串数组(注意是数组而不是切片),然后创建一个切片的结构体,包括了三个值,
==第一个是指向数组首元素的指针,第二个是切片的长度(切片的长度而不是数组的长度),第三个是数组的长度(准确的
说是切片的容量,在本例中切片的容量和数组的真实长度相同).
==
看到结构体中的三个内容我想大家应该大致了解了切片到底是如何实现的.下面我们再举一个例子

test := make([]string,10,100)

此时切片结构体中的第二个变量和第三个变量的值是多少呢?是的,分别是10和100.我们来仔细说说这两个值到底
在内存中指代了什么.我们创建了test,make会将我们的第三个参数100作为分配内存的依据,也就是说,实际上我们
分配了一个长度为100个string类型的连续的内存空间.而这个10则是当前test可以直接访问的内存,也就是说,在
==编写代码的时候,只有这个10才是对程序员"可见的".只有当我们用append()来添加元素的时候,test的可见长度才
会随之增大.如果我们直接访问test[10],那么会panic
==.
回到第一个例子,我们通过[]string直接创建切片再进行添加操作会发生什么呢?我们贴一段代码

test := []string{"test"}
println(test)
test = append(test,"test1")
println(test)

[1/1]0xc820041f08
[2/2]0xc82000e400

可以看到,test的内存已经发生了变化.也就是说,如果我们使用append()的时候,切片的长度已经大于我们最初
分配的内存,此时切片会重新分配内存,分配的规则就是将当前长度转换为二进制然后左移一位.
==注意:此处的的重新分配内存是指底层数组扩容,开辟新的空间,而slice中改变的是slice结构体中存储的数组其实内存地址,容量和长度,slice的指正并不会发生变化,仅仅是内容改变==

Slice与Array

刚才我们提到过,切片的实现是数组,那么二者之间有什么关系?切片就像是指针,数组则是切片具体指向的结构.
下面我们详细说说.

test := make([]string,1)
println(test)
test = append(test,"test1")
println(test)
test = append(test,"test2")
println(test)
test1 := test
println(test1)
test1 = append(test1,"test3")
println(test1)
test = append(test,"test4")
println(test1[3])
println(test[3])

[1/1]0xc820041f08
[2/2]0xc820070020
[3/4]0xc820072040
[3/4]0xc820072040
[4/4]0xc820072040
test4
test4

我们创建了一个切片test,实际上我们是创建了一个数组作为底层结构,然后创建了一个切片结构体并返回.如果
我们创建一个新的切片test1并将test的值赋给test1,可以看到,在对test1做append()的时候内存并没有变化,
也就是说,这两个切片共享着同一个底层数组.下面的println更是可以说明这点,我们在对test1进行append()之后
test的可访问长度依旧是3,所以当我们对test进行append之后,"test4"会覆盖掉test1中的"test3".
让我们更详细的说一下test和test1的关系.我们在创建test的时候,test负责创建了一个Array,那么经过了两次
append()之后,test的长度为3,容量为4.此时我们新建了一个test1,test1指向的也是Array.然后我们对test1调用
了一次append(),此时Array的第四个位置已经被占用了,此时test1的长度和容量都是4,但是test的长度依旧是3,容量
是4,这意味着Array的第四个位置对test是不可见的.如果我们调用test[3]会panic.因为test的不可见性,所以test并
==不会知道第四个位置的状态,那么进行append()的时候它仍然会将数据放入test[3]中.也就是Array的第四个位置==.此时
就造成了覆盖

append()函数默认在切片的末尾添加内容,而我们用make创建切片的时候"顺便"初始化了指定长度的内容.也就是说,
append()会绕过这些被初始化的内容在末尾开始添加.我在写程序的时候因为忽略了这一点导致了很大的问题...

当我们只想返回切片的某一部分的时候,譬如用test[:3]来返回切片的前三个位置,就如之前提到的,如果我们对新建的
切片进行append()操作时会覆盖掉原来切片指向的Array的第四个位置.

append函数

append函数接受两个参数,第一个参数为切片,第二个参数为可变参数,可以是该类型的值.同样我们也可以用[]string{"test"}...
的方式传入另一个切片.而append的函数的返回值也是一个切片.也就是说,append函数并没有修改我们传入的切片,而是建立了一个
新的切片返回.

test1 := append(test,"strings")

我们假设test是一个string类型的切片,长度为2容量为4.那么进行了append操作之后,如果我们打印出test的值发现它并没有变化,
长度依旧为2,而test1则指向与test相同的底层数组,而长度为3.相信讲到这里大家都明白发生了什么

建立切片并指定容量

刚才我们提到过,如果切片操作不当是容易造成覆盖的.比如我想返回某一个切片当中的某一部分元素,那么我对切片就再进行了一次切片操作.

test1 = test[:2]

我们假设test长度为5,容量为10.此时的test1的长度应该为2,那么容量呢?
容量为10.也就是说test1的容量会和test同步.那么此时会发生什么?没错,由于上面提到的对于切片长度所造成的不可见性,对test1使用
append()会造成覆盖,并且我们还可以通过test1 = test1[:cap(test1)]来对test1进行扩展.那么我们就失去了做切片的便捷性和意义了.
此时我们应该怎么做呢.请看下面这行代码

test1 = test[:2:2]

方括号中的第三个参数代表了test1的容量.也就是说,我们强制领test1的容量与长度相同,那么此时我们append()操作就会将test1指向到
一个新的底层数组中去,并且我们也避免了使用cap来对其进行扩展.

copy函数

copy函数比较简单,参数为两个类型相同的切片,函数会将第二个参数中的值复制到第一个参数中去.其中复制的大小则取决与两个参数中较小的
那个.
如果第二个参数较小的话,第一个参数中剩余的部分不会发生改变.
copy函数修改的是底层数组,并没有改变切片的指向.

append函数几点注意

append函数返回更新后的slice(长度和容量可能会变),必须重新用slice的变量接收,不然无法编译通过

  • slice的底层是数组,一片连续的内存,slice变量只是存储该slice在底层数组的起始位置、结束位置以及容量。

  • 它的长度可以通过起始位置和结束位置算出来,容量也可以通过起点位置到底层数组的末端位置的长度算出来,多个slice可以指向同一个底层数组。所以slice和数组指针不同,数组指针主要存储底层数组的首地址。

  • 因为Go函数传递默认是值拷贝,将slice变量传入append函数相当于传了原slice变量的一个副本,注意不是拷贝底层数组,因为slice变量并不是数组,它仅仅是存储了底层数组的一些信息。

所以说,当它改变传入的slice变量的信息,原slice变量并不会有任何变化,打印原slice变量和之前也会一模一样。该函数会返回修改后的slice变量,因为原slice并不会变,假如没有任何slice变量接收返回的值,那么此次append操作就没有意义了。所以必须要有slice变量重新接收修改后的slice变量,不然编译器会报错。Go不希望你做无意义的事,就像导入的包或定义的变量没有用上,它也会报错。

几个例子

func TestBug(t *testing.T) {
    x := []int{}
    x = append(x, 0)
    fmt.Println(fmt.Sprintf("===append 0==== len :%d====cap :%d===", len(x), cap(x)))
    fmt.Printf("%p\n", &x)
    x = append(x, 1)
    fmt.Println(fmt.Sprintf("===append 1==== len :%d====cap :%d===", len(x), cap(x)))
    fmt.Printf("%p\n", &x)
    x = append(x, 2)
    fmt.Println(fmt.Sprintf("===append 2==== len :%d====cap :%d===", len(x), cap(x)))
    fmt.Printf("%p\n", &x)
    y := append(x, 3)
    fmt.Println(fmt.Sprintf("===append 3==== len :%d====cap :%d===", len(x), cap(x)))
    fmt.Printf("%p\n", &x)
    z := append(y, 4)
    fmt.Println(fmt.Sprintf("===append 4==== len :%d====cap :%d===", len(x), cap(x)))
    fmt.Printf("%p\n", &x)
    fmt.Printf("%p\n", &y)
    fmt.Printf("%p\n", &z)
    fmt.Println(x, y, z)
    fmt.Println(fmt.Sprintf("===y==== len :%d====cap :%d===", len(x), cap(x)))
    fmt.Println(fmt.Sprintf("===z==== len :%d====cap :%d===", len(x), cap(x)))
}


输出:
===append 0==== len :1====cap :1===
0xc042149c80
===append 1==== len :2====cap :2===
0xc042149c80
===append 2==== len :3====cap :4===
0xc042149c80
===append 3==== len :3====cap :4===
0xc042149c80
===append 4==== len :3====cap :4===
0xc042149c80
0xc042149cc0
0xc042149ce0
[0 1 2] [0 1 2 3] [0 1 2 3 4]
===y==== len :3====cap :4===
===z==== len :3====cap :4===

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

推荐阅读更多精彩内容

  • 线性结构是计算机最常用的数据结构之一。无论是数组(arrary)还是链表(list),在编程中不可或缺。golan...
    _二少爷阅读 6,599评论 5 13
  • 本文翻译自Rob Pike的文章《Arrays, slices (and strings): The mechan...
    大蟒传奇阅读 4,887评论 2 8
  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
    Tuberose阅读 18,405评论 1 46
  • 今天翻看以前的一些东西,才发现原来我曾经有那么多不开心啊!那我还是喜欢现在的状态啊~ 最近刷一本大冰的《阿弥陀佛,...
    好梦不遥远阅读 326评论 8 2
  • 再读王锋老师的书在结束语有一段话,甚是喜欢,摘抄下来,时常回味。 原先以为,才华是个门槛儿;后来懂点事,觉得勤奋是...
    张俊霞阅读 278评论 1 1