我是靠谱客的博主 大胆老鼠,这篇文章主要介绍go专家编程系列(2)常见数据结构 slice切片,现在分享给大家,希望可以做个参考。

切片


文章目录

  • 切片
    • slice的特性
      • 初始化
      • 切片操作与切片表达式
          • 简单表达式
          • 扩展表达式
    • slice的实现原理
        • 扩容


slice的特性

又称动态数组,依托数组实现,可以方便地进行扩容和传递,实际使用时比数组更灵活。

复制代码
1
2
3
4
5
6
type slice struct { array unsafe.Pointer len int cap int }

以上是go中slice的声明。

初始化

  • var ints []int
  • ints := make([]int , 2 ,5)
  • ps := new([]string) //注意这里的ps是一个指针
  • array := [5]int{1,2,3,4,5} s1 := array[0:3] s2 := s1[0:1]

切片操作与切片表达式

简单表达式

a[low : hihg]
如果 a 为数组或者切片,则该表达式将切取 a[ low , high )的元素,如果 a 为string 该表达式将会生成一个string,而不是slice

复制代码
1
2
3
4
5
6
a := [5]int{1,2,3,4,5} b := a[1:4] b[0] = 2 c := b[1:2]

根据之前切片结构的声明,我们知道 slice 有三个元素,对于array(底层数组地址),着重强调,使用简单表达式生成的slice将与原数组或slice共享底层数组,新切片的生成逻辑可以理解为

复制代码
1
2
3
4
b.array = &a[low] b.len = heigh - low b.cap = len(a) - low //注意 b 的 cap 不是 len(b)

大家可以试试以下代码的输出

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func SlicePrint() { s1 := []int{1, 2} s2 := s1 s3 := s2[:] fmt.Printf("%p %p %pn", &s1[0], &s2[0], &s3[0]) } func SliceCap() { var array [10]int{1,2,3,4,5,6,7,8,9} var slice = array[5:6] var slice2 = array[9:10] fmt.Printf("len(slice) = %dn", len(slice)) fmt.Printf("cap(slice) = %dn", cap(slice)) fmt.Printf("len(slice) = %dn", len(slice2)) fmt.Printf("cap(slice) = %dn", cap(slice2)) }

在上面的例子中如果给slice2添加新元素 slice2 = append(slice2,10),原本的底层数组就不够用了,这时go会分配一段的内存空间作为底层数组,并将slice中的元素拷贝到新的数组中然后将新添加的元素加到数组中,而这段新的内存有多大呢,这在一会儿的实现原理中说。

另外,需要注意,如果简单表达式的对象是slice,那么表达式a[low : high]中 low 和 high 的最大值可以是 a 的容量,而不是 a 的长度

扩展表达式

a[low : high : max]
简单表达是生成的新slice与原数组共享底层数组避免了拷贝元素,节约内存空间的同时可能会带来一定的风险。
新slice (b := a[low:high])不仅仅可以读写 a[low] 到 a[high-1] 的元素,而且在使用append(a,x)添加新的元素还会覆盖掉 a[high]以及后面的元素

复制代码
1
2
3
4
5
a := [5]slice{1,2,3,4,5} b := a[1:4] b = append(b,0) fmt.Println(a[4]) //0

而扩展表达式就是解决这个问题的机制 ,low high max 满足 0 <= low <= high <= max <= cap(a) max用于限制新生成切片的容量,新切片的容量为 max - low

复制代码
1
2
3
4
array := [10]int a := array[5:7] //cap = 5 b := array[5:7:7] //cap = 2

slice的实现原理

slice的使用很灵活,但是想要正确使用它,就要了解它的实现原理。

  • var ints []int slice{array : nil , len : 0 , cap : 0}

  • ints := make([]int , 2 ,5) slice {array : 一段连续内存的起始地址(同时将元素全部初始化为整型的默认值 0 ) , len : 2 , cap : 5}

    • slice元素的访问
      复制代码
      1
      2
      3
      4
      5
      ints := make([]int,2,5) fmt.Pritln(ints[0]) ints = append(ints , 3) // ints 的底层数组变化为 0 0 3 0 0 但只可以访问前三个元素 之后的属于越界访问
  • ps := new([]string) slice{array : nil , len : 0 , cap : 0}

    • 这里ps 就是slice结构体的起始地址,这时slice还没有分配底层数组,如果想要向slice中添加元素需要使用内置函数append() *ps = append(*ps , "hello世界") 这样 *ps.array 指向的就是一个 stringStruct stringStruct{ str : 底层数组起始地址, len : 11}

slice 依托数组实现,底层数组对用户屏蔽吗在底层数组容量不足时可以实现自动分配并生成新的slice

扩容

扩容容量的选择遵守以下规则

  • 如果原slice的容量翻倍后仍然小于最低容量需求 cap ,就直接扩容到 cap
  • 否则,如果原slice的容量小于1024,则新slice的容量将扩大为原来的2倍
  • 如果原slice的容量大于等于1024,则新slice的容量将扩大为原来的1.25倍
    src/runtime/slice.go:growslice
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }

在此规则之上,还会考虑元素类型与内存分配规则,对实际扩张值做一些微调。比如,os常常将内存切分为 64、80、96、112 等常用的大小 go的内存分配机制会根据预估大小匹配合适的内存块分配给新slice。

最后

以上就是大胆老鼠最近收集整理的关于go专家编程系列(2)常见数据结构 slice切片的全部内容,更多相关go专家编程系列(2)常见数据结构内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(61)

评论列表共有 0 条评论

立即
投稿
返回
顶部