首页 > 其他分享 >Golang - Slice 学习笔记

Golang - Slice 学习笔记

时间:2023-08-22 11:37:16浏览次数:32  
标签:扩容 切片 Slice 容量 笔记 Golang slice 数组

Slice

1、概述:

Slice又称动态数组,依托数组实现,可以方便的进行扩容、传递等,实际使用中比数组更灵活。

2、实现原理

Slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的Slice。接下来按照实际使用场景分别介绍其实现机制。

2.1 Slcie底层结构

源码包中src/runtime/slice.go:slice定义了Slice的数据结构:

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

从数据结构看Slice很清晰, array指针指向底层数组,len表示切片长度,cap表示底层数组容量。

2.2 Slice的创建

2.2.1 使用 make 创建 slice

使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。

例如,语句slice := make([]int, 5, 10)所创建的Slice,结构如下图所示:


该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可。

2.2.2 使用数组创建Slice

使用数组来创建Slice时,Slice将与原数组共用一部分内存。

例如,语句slice := array[5:7]所创建的Slice,结构如下图所示:

切片从数组array[5]开始,到数组array[7]结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5。

数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方

2.3 slice的容量

  • 如果是make声明,slice := make([]int, 5, 10),指定了容量是几,那就是几,如果没有指定,那么默认是和长度一样。
  • 如果是数组声明,slice := array[5:7]如果没有指定最大值(也就是第三个方括号里面的参数),则容量是从开始位置到数组末尾。 如果指定了最大值(也就是第三个方括号里面的参数),则容量是从开始位置到最大值
  • 如果是 var slice []int slice = append(slice, 1, 2, 3) 这样声明slice, 目前不清楚

2.4 Slice 扩容

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

golang slice (切片) 扩容机制详解(1.18版本后)

growslice()方法**:用于 slice 的扩容

func growslice(et *_type, old slice, cap int) slice {
   //  ......
	newcap := old.cap
	doublecap := newcap + newcap    //双倍扩容(原容量的两倍)
	if cap > doublecap {   //如果所需容量大于 两倍扩容,则直接扩容到所需容量
		newcap = cap
	} else {
		const threshold = 256   //这里设置了一个 阈值 -- 256
		if old.cap < threshold {		//如果旧容量 小于 256,则两倍扩容
			newcap = doublecap   
		} else {
	    // 检查 0 < newcap 以检测溢出并防止无限循环。
			for 0 < newcap && newcap < cap {   //如果新容量 > 0  并且 原容量 小于 所需容量
			
               // 从小片的增长2x过渡到大片的增长1.25x。这个公式给出了两者之间的平滑过渡。
				newcap += (newcap + 3*threshold) / 4
                //新容量是 = 1.25 原容量 + 3/4 阈值 
                
              //当newcap计算溢出时,将newcap设置为请求的上限。
			if newcap <= 0 {   // 如果发生了溢出,将新容量设置为请求的容量大小
				newcap = cap
			}
		}
	}
}

具体情况如下:

  1. 如果请求容量 大于 两倍现有容量 ,则新容量 直接为请求容量

  2. 否则(请求容量 小于等于 两倍现有容量)

​ 如果 现有容量 小于 256 ,则新容量是原来的两倍

​ 否则:新容量 = 1.25 原容量 + 3/4 阈值 “这个公式给出了从1.25倍增长 过渡到2 倍增长,两者之间的平滑过渡。” 在此情况下,如果发生了溢出,将新容量设置为请求的容量大小.(这里的系数会随着容量的大小发生变化,从2.0到无线接近1.25)

扩容容量的选择遵循以下规则:

在 1.18 版本前,切片扩容,在容量小于1024时,以2倍大小扩容。超过1024后,以1.25倍扩容。

在1.18版本后,切片扩容,在容量小于256时,以2倍大小扩容。超过256后,以(1.25倍+0.75阈值)扩容。

使用append()向Slice添加一个元素的实现步骤如下:

  1. 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice
  2. 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice,再将新元素追加进新Slice,Slice.len++,返回新的Slice。

2.5 Slice Copy

使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取决于两个切片长度的最小值

例如长度为10的切片拷贝到长度为5的切片时,将会拷贝5个元素。

也就是说,copy过程中不会发生扩容

2.6 特殊切片

跟据数组或切片生成新的切片一般使用slice := array[start:end]方式,这种新生成的切片并没有指定切片的容量,实际上新切片的容量是从start开始直至array的结束

比如下面两个切片,长度和容量都是一致的,使用共同的内存地址:

sliceA := make([]int, 5, 10)
sliceB := sliceA[0:5]

根据数组或切片生成切片还有另一种写法,即切片同时也指定容量,即slice[start : end : cap], 其中cap即为新切片的容量,当然容量不能超过原切片实际值,如下所示:

    sliceA := make([]int, 5, 10)  //length = 5; capacity = 10
    sliceB := sliceA[0:5]         //length = 5; capacity = 10
    sliceC := sliceA[0:5:5]       //length = 5; capacity = 5

这切片方法不常见,在Golang源码里能够见到,不过非常利于切片的理解。

2.7 Slice的函数传递

首先给出结论:slice在作为函数参数进行传递时,分两种情况:

  1. 如果不发生扩容,则是引用传递,也就是函数内对切片的操作会影响函数外的切片。
  2. 如果发生扩容,则函数内的切片的内存地址发生变化,此时对函数内切片的操作不会影响函数外。

因此,如果想要函数内对切片的操作始终影响函数外,那么在传参时使用指针即可。

代码示例:

func Slicefunc2(paramSlice *[]int) {
	paramSliceV := *paramSlice
	paramSliceV[2] = 9
	for index := 0; index < 6; index++ {
		*paramSlice = append(*paramSlice, index)
	}
	fmt.Printf("Slicefunc2函数内slice的值为:%v\n", paramSlice)
}
func Slicefunc1(paramSlice []int) {
	paramSlice[2] = 9
	for index := 0; index < 6; index++ {
		paramSlice = append(paramSlice, index)
	}
	fmt.Printf("Slicefunc1函数内slice的值为:%v\n", paramSlice)
}
func main() {
	slice := make([]int, 5)
	slice[0] = 2
	slice[1] = 3
	fmt.Println("main函数中的slice: ", slice)
	Slicefunc1(slice)
	fmt.Println("经过Slicefunc1后,main函数中的slice: ", slice)
	Slicefunc2(&slice)
	fmt.Println("经过Slicefunc2后,main函数中的slice: ", slice)
}

//输出结果
//main函数中的slice:  [2 3 0 0 0]
//Slicefunc1函数内slice的值为:[2 3 9 0 0 0 1 2 3 4 5]
//经过Slicefunc1后,main函数中的slice:  [2 3 9 0 0]
//Slicefunc2函数内slice的值为:&[2 3 9 0 0 0 1 2 3 4 5]
//经过Slicefunc2后,main函数中的slice:  [2 3 9 0 0 0 1 2 3 4 5]

3. 编程Tips

创建切片时可跟据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能;

  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组,以防读写冲突

4. Slice总结

  • 每个切片都指向一个底层数组
  • 每个切片都保存了当前切片的长度、底层数组可用容量
  • 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片
  • 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而矣
  • 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片

标签:扩容,切片,Slice,容量,笔记,Golang,slice,数组
From: https://www.cnblogs.com/lxing-go/p/17648080.html

相关文章

  • GoLange:面向对象
    学习自:Go教程130页1、类定义方式:结构体+方法结构体:定义有哪些数据方法:定义结构体的方法例子:定义一个Person类//结构体定义人的属性typePersonstruct{namestringageint}//方法定义人的行为func(pPerson)Say(){fmt.Println("mynameis",p.n......
  • object - c 语言基础 进阶笔记 随笔笔记
    重点知识Engadget(瘾科技)StackOverFlow(栈溢出)Code4Apprespon魏先宇的程序人生第一周快捷键:Alt+上方向键跳到最上面 Alt+下方向键跳到最下面      Alt+左方向键跳到最左面  Alt+右方向键跳到最右面      Alt+shift+方向键 可以批量复制内容,在按方向......
  • 【分享】字符串常见函数学习笔记
    字符串常见函数学习笔记 1:查找函数:find()//返回值string::nposstrings="goodgoodstudy,daydayup";intpos1=s.find("good");//查找子串"good"intpos2=s.find("good",4);//从下标4开始,查找子串"good"cout<<pos1<......
  • GoLang:接口
    学习自:Go教程119页1、说明接口是为了定义某些标准,接口本身不需要实现这些标准。2、定义接口中不能有任何数据字段,只能有函数声明type接口名称interface{函数声明} 接口中嵌入另一个接口但是嵌入的接口中方法不能重名,把自己嵌入自己3、例子1)常规用法定义一......
  • 资讯_Windows 8笔记本电脑关机后电源灯要亮一段时间,是否正常?
    Windows8笔记本电脑关机后电源灯要亮一段时间,是否正常故障现象:随着Windows8的普及,多次遇到用户反馈安装Windows8的笔记本在执行关机动作后,屏幕关闭之后电源等指示灯还要亮几十秒,甚至几分钟不等。——此现象其实是由于Windows8的混合关机特质所致,并不属于故障的范畴。原因分析:在W......
  • golang 学习笔记 -- for
    forrange 遍历取不到所有元素的指针orgItems:=[]int{1,2,3}varnewItems[]*intfor_,item:=rangeorgItems{fmt.Println(item)//123fmt.Printf("%p\n",&item)//每次地址都是相同的newItems=append(newItems,&item)}for_,ite......
  • GoLang:指针
    学习自:GoLang教程110页1、普通指针说明指针变量用于存放其他变量的地址无论指向什么类型的变量,指针的实际内存占用都是一样的:32位4B,64位8B:varp1*int;varp2*float64;varp3*bool;fmt.Println(unsafe.Sizeof(p1))//8fmt.Println(unsafe.Sizeof(p2))//8fmt.Println(u......
  • 【学习笔记】DSU on Tree
    概述DSUonTree即树上启发式合并,重点不在“合并”,而在利用树链剖分的性质对子树问题进行复杂度正确的分治。算法流程递归处理轻儿子的答案递归处理重儿子的答案重新遍历轻儿子子树,计算当前子树的答案如果当前节点是轻儿子,重新遍历整棵子树,清除答案发现一个节点......
  • 「学习笔记」扩展 KMP(Z 函数)
    对于个长度为\(n\)的字符串\(s\)。定义\(z[i]\)表示\(s\)和\(s[i,n-1]\)(即以\(s[i]\)开头的后缀)的最长公共前缀(LCP)的长度。\(z\)被称为\(s\)的Z函数。这里注意,在Z函数中,\(z[0]=0\),但是根据LCP的定义,\(z[0]=n\),具体应该为何值,根据题目意思来判断。本文更偏......
  • 【Boost】boost.log 要点笔记
    常用简写:namespacelogging=boost::log;namespacesrc=boost::log::sources;namespaceexpr=boost::log::expressions;namespacesinks=boost::log::sinks;namespaceattrs=boost::log::attributes;namespacekeywords=boost::log::keywords;要点:结构图要牢......