关于后端:GO-中-slice-的实现原理

45次阅读

共计 3587 个字符,预计需要花费 9 分钟才能阅读完成。

GO 中 slice 的实现原理

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

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

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

slice 是什么?

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

看看 GO 的 src/runtime/ 下的 slice.go 源码,咱们能够找到 slice 的数据结构

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 的一种数据类型,是对数组的一个间断片段的援用

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

  • array

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

  • len

指的是切片的长度

  • cap

指的是切片的容量

有没有感觉,切片和咱们理解的数组如同是一样的,然而如同又不一样

slice 和 数组的区别是啥?

大略有如下几个区别

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

在 GO 外面,传递数组,是通过拷贝的形式

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

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

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

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

切片是援用类型,切片的长度是不固定的,可扩大的,GO 外面操作切片真的是香

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

当应用切片的时候,数组容量不够的时候,这个底层数组会主动重新分配,生成一个新的 切片 留神,这里是生成一个新的切片

如何创立 slice

创立一个新的切片有如下几种形式:

  • 应用 make 办法创立 新的切片
  • 应用数组赋值的形式创立新的切片

应用 make 办法创立 新的切片

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

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 个元素复制给到新的切片

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,大家应该不会感觉奇怪

可是为什么 cap 等于 4?

起因如下:

数组的索引是从 0 开始的

上述代码 arr[4:6] 指的是将数组的下标为 4 开始的地位,下标为 6 的为完结地位,这里是不蕴含 6 本人的

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

即失去上述的后果,len == 2,cap == 4

不过这里还是要留神,切片元素对应的地址,还是这个数组元素对应的地址,应用的时候须要小心

slice 扩容原理是什么?

咱们就来模仿一下

  • 新建一个 长度为 4,容量为 4 的切片
  • 向切片中增加一个元素
  • 打印最终切片的详细情况

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

咱们来看看切片扩容的规定是这样的:

  • 如果原来的切片容量小于 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 不是一个空对象,是一个失常的对象,然而对象外面的为空

总结

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

欢送点赞,关注,珍藏

敌人们,你的反对和激励,是我保持分享,提高质量的能源

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

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

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

正文完
 0