由浅入深聊聊Golang的slice

Posted 咖啡色的羊驼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由浅入深聊聊Golang的slice相关的知识,希望对你有一定的参考价值。

前言

今天本来想去外地玩耍,结果睡过头错过了动车,只好总结一下slice,希望能与slice之间做一个了断。

文章由浅入深,遵从能用代码说话就不bb的原则。

正文

1.基本操作

1.1 声明

var stringSlice []string
stringSlice := []string"咖啡色的羊驼"

var intSlice []int64
intSlice := []int18

和数组的区别:就是**[]括号里头不加东西**。

初始化的的一些默认值:

func main() 
	var stringSlice []string
	var intSlice []int64
	fmt.Printf("stringSlice ==> 长度:%v \\t地址:%p \\t零值是否nil:%v \\n",len(stringSlice),stringSlice, stringSlice==nil)
	fmt.Printf("intSlice ==> 长度:%v \\t地址:%p \\t零值是否nil:%v",len(intSlice),intSlice, intSlice==nil)

这里需要注意的是:slice的key必须是数字 && 0开始逐渐增加

1.2 增删改查

// 增
func add(slice []interface, value interface) []interface 
	return append(slice, value)


// 删
func remove(slice []interface, i int) []interface 
	return append(slice[:i], slice[i+1:]...)


// 改
func update(slice []interface, index int, value interface) 
	slice[index] = value


// 查
func find(slice []interface, index int) interface 
	return slice[index]

这里需要注意的是:
1.slice的增加需要依赖于append,这里会涉及到扩容机制(后文会说)
2.删除的话,只能是通过切割的方式重拼了,由于slice是引用类型,存的是指针,性能上不会有太多影响

1.3 插入 & 遍历 & 清空

// 插入 
func insert(slice *[]interface, index int, value interface) 
	rear := append([]interface, (*slice)[index:]...)
	*slice = append(append((*slice)[:index], value), rear...)


// 遍历
func list(slice []interface) 
	for k, v := range slice 
		fmt.Printf("k:%d - v:%d", k,v)
	


// 清空 
func empty(slice *[]interface) 
	*slice = append([]interface)
	//    *slice = nil

1.3 复制

// 复制
func main() 
	intSlice := []int1,2,3,4,5,6
	copySlice1 := make([]int,0,10)

	_ = copy(copySlice1,intSlice)
	fmt.Printf("长度为0的时候:%v\\n",copySlice1)

	copySlice2 := make([]int,6,10)

	_ = copy(copySlice2,intSlice)
	fmt.Printf("长度为6的时候:%v",copySlice2)

这里需要注意的是:要保证目标切片有足够的大小,注意是大小,而不是容量

2.slice的深入了解

2.1 slice的基础数据结构 & 图

slice的基础数据结构:

type slice struct 
    array unsafe.Pointer
    len   int
    cap   int

字段说明
array指向指针,指向一个底层数组
lenslice中元素的个数
capslice的容量,允许slice中元素增长的最大个数

这里的array需要单独说下,这里是指针类型,也说明了slice是引用类型


在slice底层,指针指向的是另一个数组。

还是有必要看一下源码中的实现:

// 创建一个slice
func makeslice(et *_type, len, cap int) slice 
    // 检查目标类型的最长长度,slice的len和cap都必须小于这个值 
    maxElements := maxSliceCap(et.size)
    if len < 0 || uintptr(len) > maxElements 
        panic(errorString("makeslice: len out of range"))
    

    // !!! len必须<= cap !!!
    if cap < len || uintptr(cap) > maxElements 
        panic(errorString("makeslice: cap out of range"))
    

    // 申请内存
    p := mallocgc(et.size*uintptr(cap), et, true)

    // 返回一个slice
    return slicep, len, cap


2.2 slice切割的底层变化

func main() 
	x:=[]int1,2,3,4
	y:=x[1:4]
	fmt.Println(y)


// 输出:[2 3 4]

这里需要注意的是:切割遵从左开右闭的原则,就是[1:4],取得是第二个元素到第四个以下的,不包括第四个

来一波图解:

2.3 slice的扩容机制 & 实战例子

slice扩容机制还是比较有意思的,上源码:

func growslice(et *_type, old slice, cap int) slice 
    ...
    
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap 
        newcap = cap
     else 
        if old.len < 1024 
            newcap = doublecap
         else 
            for newcap < cap 
                newcap += newcap / 4
            
        
    

    ...
    
    return slicep, old.len, newcap

白话文讲解:如果新cap的大小是当前2倍以上,则增长到新的cap大小;否则,如果当前cap大小<1024,则按照2倍增长,不然就每次就按照当前大小的1/4增长,直到大小>=新的cap大小。

前文很多操作都是基于append的,那么slice在append的时候,如果发生了扩容,那么底层的数组会重建,同时拷贝老的数据到新数组里头。

举个例子:

func main() 
	initSlice := []int1
	// 进行扩容到2
	initSlice = append(initSlice, 2)
	// 进行扩容到4
	initSlice = append(initSlice, 3)
	x := append(initSlice, 4)
	y := append(initSlice, 5)
	fmt.Println(initSlice, x, y)

会输出:

[1 2 3] [1 2 3 5] [1 2 3 5]

图解说明:


根据前文介绍的扩容机制,initSlice的扩容轨迹是1-2-4。而slice只是引用类型,所以x和y只是copy了initSlice的指针,他们三个都是指向同一个底层数组,所以最后第四个坑被y给覆盖了。

再举一个正好遇到扩容时候的例子:
我们知道扩容时候是会生成新的底层数组,然后拷贝老的数组值。

func main() 
	initSlice := []int1
	// 此时扩容1-2,并且全部装满
	initSlice = append(initSlice, 2)
	
	// 以下任一append都会引发扩容
	x := append(initSlice, 3)
	y := append(initSlice, 4)
	fmt.Println(initSlice, x, y)

输出:

[1 2] [1 2 3] [1 2 4]

图解:

由于都遇到了扩容,所以x与y各自另立门户,新建数组,slice指向的底层数组也不同了所以互不干扰了。

3.注意点

3.1 函数传参

func main()  
	initSlice := []int1,2,3
	fmt.Printf("刚开始时候:%v\\n",initSlice)
	doSomeThing(initSlice)
	fmt.Printf("一番操作后:%v\\n",initSlice)


func doSomeThing(s []int)  
	s[0]=88
	s = append(s, 10)
	fmt.Printf("函数返回前:%v\\n",s)

输出:

刚开始时候:[1 2 3]
函数返回前:[88 2 3 10]
一番操作后:[88 2 3]

这里面的变化情况抓住一个点:就是发送扩容时候底层数组是新建的!

然而我想说的是:函数传slice,由于是引用类型,所以是会改变原值的,这时候需要考虑扩容新底层数组问题。

如果你觉得有收获~可以关注我的公众号【咖啡色的羊驼】~第一时间收到我的分享和知识梳理~

以上是关于由浅入深聊聊Golang的slice的主要内容,如果未能解决你的问题,请参考以下文章

由浅入深聊聊Golang的sync.Map

由浅入深聊聊Golang的sync.Map

由浅入深聊聊Golang的context

由浅入深聊聊Golang的map

由浅入深聊聊Golang的sync.Pool

由浅入深聊聊Golang的sync.Pool