GO 中 slice 的实现原理

GO 中 slice 的实现原理.jpg

上次我们分享的字符串相关的内容咱回顾一下

  • 分享了字符串具体是啥
  • GO 中字符串的特性,为什么不能被修改
  • 字符串 GO 源码是如何构建的 ,源码文件在 src/runtime/ 下的 string.go
  • 字符串 和 []byte 的由来和应用场景
  • 字符串与 []byte 相互转换

要是对GO 对 字符串 的编码还有点兴趣的话, 欢迎查看文章 GO 中 string 的实现原理

slice 是什么?

有没有觉得很熟悉,上次分享的 string 类型 对应的数据结构 的前两个参数 与 切片的数据结构的前两个参数是一样的

image

看看GO 的 src/runtime/ 下的 slice.go 源码,我们可以找到 slice的数据结构

image
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

// unsafe.Pointer 类型如下
// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
//  - A pointer value of any type can be converted to a Pointer.
//  - A Pointer can be converted to a pointer value of any type.
//  - A uintptr can be converted to a Pointer.
//  - A Pointer can be converted to a uintptr.
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.
type Pointer *ArbitraryType

切片GO 的一种数据类型 , 是对数组的一个连续片段的引用

切片的底层结构是一个结构体,对应有三个参数

image
  • array

是一个unsafe.Pointer指针,指向一个具体的底层数组

  • len

指的是切片的长度

  • cap

指的是切片的容量

有没有觉得,切片和我们了解的数组好像是一样的,但是好像又不一样

slice 和 数组的区别是啥?

image

大概有如下几个区别

  • 数组是复制传递的,而切片是引用传递的

在GO 里面,传递数组,是通过拷贝的方式

传递切片是通过引用的方式,这里说的引用,指的是 切片数据结构中array字段,其余字段默认是值传递

  • 数组是相同类型的长度固定的序列

数组是相同类型的,一组内存空间连续的数据,他的每一个元素的数据类型都是一样的,且数组的长度一开始就确定好了,且不能做变动

  • 切片是一个结构,是一个数据对象,且对象里面有 3 个参数

切片是引用类型,切片的长度是不固定的,可扩展的,GO 里面操作切片真的是香

当然,切片也是离不开数组的,因为他的array指针就是指向的一个底层数组,这个底层数组,对用户是不可见的

当使用切片的时候,数组容量不够的时候,这个底层数组会自动重新分配,生成一个新的 切片注意,这里是生成一个新的切片

如何创建 slice

创建一个新的切片有如下几种方式:

  • 使用make 方法创建 新的切片
  • 使用数组赋值的方式创建新的切片

使用make 方法创建 新的切片

新建一个 len 为 4,cap 为7 的切片:

image
func main(){
   mySlice := make([]int,4,7)
   
   // 此处的遍历 长度是 len 的长度
   for _,v :=range mySlice{
      fmt.Printf("%v",v)
   }
}

上述代码运行结果为

0000

为什么不是 7 个 0,而是4 个

这里要注意了:

此处遍历遍历切片的长度是 切片的 len 值, 而不是切片的容量 cap

使用数组赋值的方式创建新的切片

  • 创建一个 长度 为 8,数据类型为 int 的数组
  • 数组的第5个元素和第6个元素复制给到新的切片
image
func main(){

   arr := [8]int{}

   mySlice := arr[4:6]

   fmt.Println("len == ", len(mySlice))
   fmt.Println("cap == ", cap(mySlice))

   // 此处的遍历 长度是 len 的长度
   for _,v :=range mySlice{
      fmt.Printf("%v",v)
   }
}

上述代码运行结果为

len ==  2
cap ==  4
00

根据代码执行情况,打印出 00,大家应该不会觉得奇怪

image

可是为什么 cap 等于 4?

原因如下:

数组的索引是从 0 开始的

上述代码 arr[4:6] 指的是将数组的下标为 4 开始的位置,下标为 6 的为结束位置,这里是不包含6自己的

根据 GO 中切片的原理,用数组复制给到切片的时候,若复制的数组元素后面还有内容的话,则后面的内容都作为切片的预留内存

即得到上述的结果,len == 2, cap == 4

不过这里还是要注意,切片元素对应的地址,还是这个数组元素对应的地址,使用的时候需要小心

image

slice 扩容原理是什么?

我们就来模拟一下

  • 新建一个 长度为 4 ,容量为 4 的切片
  • 向切片中添加一个元素
  • 打印最终切片的详细情况
image
func main(){

   mySlice := make([]int,4,4)
   mySlice[0] = 3
   mySlice[1] = 6
   mySlice[2] = 7
   mySlice[3] = 8

   fmt.Printf("ptr == %p\n", &mySlice)
   fmt.Println("len == ", len(mySlice))
   fmt.Println("cap == ", cap(mySlice))

   // 此处的遍历 长度是 len 的长度
   for _,v :=range mySlice{
      fmt.Printf("%v ",v)
   }

   fmt.Println("")


   mySlice = append(mySlice,5)

   fmt.Printf("new_ptr == %p\n", &mySlice)
   fmt.Println("new_len == ", len(mySlice))
   fmt.Println("new_cap == ", cap(mySlice))

   // 此处的遍历 长度是 len 的长度
   for _,v :=range mySlice{
      fmt.Printf("%v ",v)
   }
}

运行上述代码,有如下效果:

ptr == 0x12004110
len ==  4
cap ==  4
3 6 7 8
new_ptr == 0x12004110
new_len ==  5
new_cap ==  8
3 6 7 8 5

根据案例,相信大家或多或少心里有点感觉了吧

向一个容量为 4 且长度为 4 的切片添加元素,我们发现切片的容量变成了 8

image

我们来看看切片扩容的规则是这样的:

  • 如果原来的切片容量小于1024

那么新的切片容量就会扩展成原来的 2 倍

  • 如果原切片容量大于等于1024

那么新的切片容量就会扩展成为原来的1.25倍

我们再来梳理一下上述扩容原理的步骤是咋弄的

上述切片扩容,大致分为如下 2 种情况:

  • 添加的元素,加入到切片中,若原切片容量够

那么就直接添加元素,且切片的len ++ ,此处的添加可不是直接赋值,可是使用 append函数的方式,例如

func main(){

    mys := make([]int,3,5)
    fmt.Println("len == ", len(mys))
    fmt.Println("cap == ", cap(mys))

    mys[0] = 1
    mys[1] = 1
    mys[2] = 1
    // mys[3] = 2   会程序崩溃
    mys = append(mys,2)

    fmt.Println("len == ", len(mys))
    fmt.Println("cap == ", cap(mys))
    for _,v :=range mys{
        fmt.Printf("%v",v)
    }
}
  • 若原切片容量不够,则先将切片扩容,再将原切片数据追加到新的切片中

简单说一下空切片和 nil 切片

平时我们在使用JSON 序列化的时候,明明切片为空

为什么有的 JSON 输出是[] , 有的 JSON 输出是 null

我们来看看这个例子

func main(){
   // 是一个空对象
   var mys1 []int
   // 是一个对象,对象里面是一个切片,这个切片没有元素
   var mys2 = []int{}

   json1, _ := json.Marshal(mys1)
   json2, _ := json.Marshal(mys2)

   fmt.Println(string(json1))
   fmt.Println(string(json2))
}

运行结果为

null
[]

原因是这样的:

  • mys1 是一个空对象
  • mys2 不是一个空对象,是一个正常的对象,但是对象里面的为空
image

总结

  • 分享了切片是什么
  • 切片和数组的区别
  • 切片的数据结构
  • 切片的扩容原理
  • 空切片 和 nil 切片的区别

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

image

好了,本次就到这里,下一次 GO 中 map 的实现原理分享

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

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

推荐阅读更多精彩内容