Go Slice详解

slice的存储结构

slice代表变长的序列,它的底层是数组。一个切片由3部分组成:指针、长度和容量。指针指向底层数组,长度代表slice当前的长度,容量代表底层数组的长度。
换句话说,slice自身维护了一个指针属性,指向它底层数组的某些元素的集合。

type slice struct {
    array unsafe.Pointer // 指向底层数组
    len int // 长度
    cap int // 容量
}

创建切片

一般有如下方法创建切片。

  1. 通过make函数创建
slice := make([]T, 5)

上述代码创建了一个整型切片,其长度为5。若不传入容量的大小,则容量和长度相同。

slice := make([]T, 3, 5)

上述代码创建了一个整型切片,其长度为3,容量为5。

  1. 通过字面量创建切片
slice := []int{1, 2, 3, 4}

上述代码创建了长度和容量均为4的整型切片。

  1. 通过切片创建切片
slice[i:j:k]

i代表起始位置,切片的长度为(j-i),切片的容量为(k-i)。如果没有指定k,则表示切到底层数组的尾部。此外还有几种简化形式:

slice[i:] // 从i切到尾部
slice[:j] // 从头部切到j,不包括j
slice[:] // 从头切到尾

nil slice和空slice

声明一个slice,但是不初始化,这个slice就是nil slice。nil slice表示它的指针为nil,也就是这个slice不会指向底层数组,因此它的长度和容量都为0。

var slice []int

创建一个长度为0的slice,就是空slice。空slice的长度和容量也为0,但是它会指向一个底层数组,只不过底层数组是长度为0的空数组。

slice := make([]int,0)

copy()函数

可以将一个slice拷贝到另一个slice中。copy()函数表示将src拷贝到dst。若src比dst长,则截断;若src比dst短,则只拷贝src的部分。返回值是拷贝成功的元素数量。

func copy(dst, src []Type) int

示例:

s1 := []int{11, 22, 33}
s2 := make([]int, 5)
s3 := make([]int,2)

num := copy(s2, s1)
copy(s3,s1)

// [11 22 33] 0xc0000160a8
fmt.Printf("the value of s1 is %v, the value of ptr of s1 is %p\n", s1, s1) 
// [11,22,33,0,0] 0xc00001c120
fmt.Printf("the value of s2 is %v, the value of ptr of s2 is %p\n", s2, s2)
// [11,22] 0xc00001a2d0 
fmt.Printf("the value of s3 is %v, the value of ptr os s3 is %p\n", s3, s3) 

s1拷贝到s2时,因s1的长度小于s2的长度,只拷贝s1的部分,则s2为[11,22,33,0,0];s1拷贝到s3时,因s3的长度小于s1的长度,所以截断s1,只拷贝前两个元素,则s3为[11, 22]。
copy()操作只是拷贝内容,各切片的底层数组仍然是独立的。

append()函数

使用append()函数可以追加元素。
在append时,如果切片的容量已经不能容纳将要追加的数据,就会创建一个新的扩容后的底层数组,将之前的数据拷贝过去后,再执行扩容操作;如果切片的容量足以容纳,那么就会在原数组执行扩容操作。
目前扩展底层数组的逻辑为:按照当前底层数组长度的2倍进行扩容;如果底层数组的长度超过1000,将按照125%扩容。
下述代码分别测试了在不会扩容和会扩容的前提下,执行append操作的结果。

func main() {
  var slice = []int{1, 2, 3, 4, 5} // len = 5; capacity = 5
  var newSlice = slice[1:3]        // len = 2; capacity = 4(已经使用了两个位置,还有两个位置可以append)

  fmt.Printf("%p\n", slice)    // 0xc00001c120
  fmt.Printf("%p\n", newSlice) // 0xc00001c128; newSlice的地址指向的是slice[1]的地址,因此底层使用的是同一个数组

  fmt.Printf("%v\n", slice)    // [1 2 3 4 5]
  fmt.Printf("%v\n", newSlice) // [2 3]

  newSlice[1] = 6              // 更改后slice、newSlice都改变了
  fmt.Printf("%v\n", slice)    // [1 2 6 4 5]
  fmt.Printf("%v\n", newSlice) // [2 6]

  newSlice = append(newSlice, 7, 8) // append操作之后,array的len和capacity不变, newArray的len变为4,capacity仍然为4
  fmt.Printf("%v\n", slice)         //[1 2 6 7 8]; newSlice改变了底层数组的内容,所以slice的内容也变了
  fmt.Printf("%v\n", newSlice)      //[2 6 7 8]

  newSlice = append(newSlice, 9, 10) // newSlice的len已经等于cap,再次append会创建一个新的底层数组(已扩容),并将array指向的底层数组拷贝过去,并追加新值。
  fmt.Printf("%p\n", slice)          // 0xc00001c120; slice指向的底层数组未改变
  fmt.Printf("%p\n", newSlice)       // 0xc00009e000; newSlice指向的底层数组有改变
  fmt.Printf("%v\n", slice)          // [1 2 6 7 8]
  fmt.Printf("%v\n", newSlice)       // [2 6 7 8 9 10]
}

slice传参

在Go语言中,函数的参数都是按值传递的,因此在调用函数时,会将参数的副本传递给函数。在传递slice时,虽然传递的是副本,但是副本同样指向了源slice的底层数组,所以在函数内部修改slice,有可能会影响到底层数组,进而影响到其他slice。

func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  change(slice)
  // [3 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice []int) {
  slice[0] = 3
  // [3 2] 0xc00000e090 0xc00001a2d0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
}

上述代码将slice传入change()函数并将slice[0]的值修改为3。在main()函数中打印出调用前后的slice值,调用前为[1,2],调用后为[3,2]。且无论是在main()函数中还是change()函数中,slice指向的底层数组的地址都是同一个。
但是若在函数内部调用append()函数,可能会生成一个新的底层数组。

func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  change(slice)
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice[0] = 4
  // [4 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice []int) {
  slice = append(slice, 3)
  // [1 2 3] 0xc00000e090 0xc0000180e0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
}

上述代码在change()内部为slice追加元素3,由于slice的长度和容量均为2,append()操作会导致slice的副本指向一个新的底层数组,因此slice的副本和slice指向的底层数组不再为同一个。
在调用change()后,将slice下标为0的值修改为4,输出slice的值为[4,2]而非[1,2,3],再次说明了change()函数内部的slice和main()函数的slice已经不再指向同一个底层数组。
如果希望获取到函数调用后的slice(),有如下两种方法:

  1. 函数调用返回slice
func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice = change(slice)
  // [1 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice[0] = 4
  // [4 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice []int) []int {
  slice = append(slice, 3)
  // [1 2 3] 0xc00000e090 0xc0000180e0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return slice
}
  1. 参数传入指针
func main() {
  slice := []int{1, 2}
  // [1 2] 0xc00000e048 0xc00001a2d0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  change(&slice)
  // [1 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  slice[0] = 4
  // [4 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("slice is %v, addr is %p, ptr is %p\n", slice, &slice, slice)
  return
}

func change(slice *[]int) {
  *slice = append(*slice, 3)
  // [1 2 3] 0xc00000e048 0xc0000180e0
  fmt.Printf("in function. slice is %v, addr is %p, ptr is %p\n", *slice, slice, *slice)
}

切片的地址和切片的指针指向的地址

func main() {
  slice := make([]int, 2, 5)
  slice1 := slice
  slice[1] = 3
  fmt.Printf("slice is %v, slice1 is %v\n", slice, slice1)                                 // [0 3] [0 3]
  fmt.Printf("addr of slice is %p, addr of slice1 is %p\n", &slice, &slice1)               // 0xc00000e048 0xc00000e060
  fmt.Printf("value of ptr of slice is %p, value of ptr of slice1 is %p\n", slice, slice1) // 0xc00001c120 0xc00001c120
}

可以看出,slice代表了其指针指向的底层数组的地址,&slice代表了slice自身的地址。
由于slice和slice1指向相同的底层数组,所以地址相同;但是slice和slice1为不同的变量,所以自身所在的地址是不同的。

参考

Go基础系列:Go slice详解

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

推荐阅读更多精彩内容