go专家编程系列(2)常见数据结构 slice

本文探讨了Go语言中切片的特性,包括其动态数组性质、底层实现结构以及初始化过程。重点讲解了切片操作,从简单表达式到扩展表达式,以及如何避免元素拷贝。此外,剖析了切片扩容规则及其背后的原理,帮助读者理解如何有效利用切片以节省内存并确保正确扩展。
摘要由CSDN通过智能技术生成

切片



slice的特性

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

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

a := [5]int{1,2,3,4,5}
b := a[1:4]
b[0] = 2
c := b[1:2]

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

b.array = &a[low]
b.len = heigh - low
b.cap = len(a) - low //注意 b 的 cap 不是 len(b)

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

func SlicePrint() {
        s1 := []int{1, 2}
        s2 := s1
        s3 := s2[:]
        fmt.Printf("%p %p %p\n", &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) = %d\n", len(slice))
        fmt.Printf("cap(slice) = %d\n", cap(slice))
        
        fmt.Printf("len(slice) = %d\n", len(slice2))
        fmt.Printf("cap(slice) = %d\n", 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]以及后面的元素

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

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元素的访问
      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
	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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值