您的位置:首页 > 房产 > 建筑 > 网站举报网_企业网站模板下载网址_长春疫情最新消息_进行seo网站建设

网站举报网_企业网站模板下载网址_长春疫情最新消息_进行seo网站建设

2025/5/1 2:18:15 来源:https://blog.csdn.net/2303_79710813/article/details/147561120  浏览:    关键词:网站举报网_企业网站模板下载网址_长春疫情最新消息_进行seo网站建设
网站举报网_企业网站模板下载网址_长春疫情最新消息_进行seo网站建设

本小节,我们来讲一下切片

一.切片的回顾

切片(slice)是一个拥有相同数据类型的可变长度的序列,他是基于数组类型的一层封装,非常灵活,支持动态扩容。

切片是一个引用类型,它的内部包括地址,长度和容量。

用于快速操作一块数据集合。

如下图所示:

1.1 切片的定义

var a []T

T 就代表类型。

package mainimport "fmt"func main() {//声明切片var a []stringvar b = []int{}var c = []bool{false, true}//var d = []bool{false, true}fmt.Println(a)fmt.Println(b)fmt.Println(c)fmt.Println(a == nil)fmt.Println(b == nil)fmt.Println(c == nil)// fmt.Println(c == d) 切片是引用,不支持直接比较,只能和nil比较
}
  • 这里可以会看到这个 var a []string和var b = []int{},为什么前者是nil而后者不是?

原因是后者已经初始化了,已经给他分配了一个arr的地址,而前者并未初始化,它里面的指针是指向nil

  • 其次就是切片不可以进行比较

1.2 make函数构造切片

使用make函数可以构造我们想要的len和cap

make([]T,size,cap)

make函数构造出来的slice,是初始化后的,除了make还是,还可以通过[]int{}的形式,生成的也是一个初始化的slice。

1.3 切片的操作

  • 切片支持对切片在进行一个切片
  • 切片的赋值是浅拷贝(后续会在底层简介)
  • 切片的遍历和数组一样
  • append()函数为切片添加元素
func main() {s1 := []string{"shy", "haha", "xixi"}//s1[3]="yiyi"  这样是不可以添加的s1 = append(s1, "succeed")//必须用原来的切片去接收返回值。fmt.Println(s1)}结果
[shy haha xixi succeed]
  • 切片的深拷贝:使用copy函数
package mainimport "fmt"func main() {s1 := []int{1, 2, 3}//浅拷贝s2 := s1s3 := make([]int, 5, 10)//var s3 []int // 如果这样复制的话,打印的结果是[]// 因为这个s3并没有被分配对应的内容空间// 深拷贝copy(s3, s1)fmt.Println(s1, s2, s3)
}

1.4 注意事项

那切片可以删除中间的元素嘛?或者说可以向中间添加元素嘛?

先说添加元素

  • append()函数之前说过,就是在末尾添加元素。
  • 那如何向中间添加元素呢
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
a = append(append(a[:1], 1), a[1:]...)

这样不就可以了吗,我取我要添加的位置,然后加上,再把他后面的填进去就ok了。

如果这样,那你就大错特错了,其实这里你已经把底层数组改变了,看下面的具体例子

在你的第一步append的时候就已经把这个底层数组改变了,索引后续的添加是在你改变的这个数组的前提之下进行的一个添加,所以就导致了上述的错误

那有没有方法向中间加入元素呢?

可以,但是需要另开一个新的空间来实现,就像这样

通过一个新开的切片,从而在不改变底层的情况下实现。

package mainimport "fmt"func main() {s := []int{1, 2, 3, 4, 5}index := 2value := 99// 手动调整切片s = append(s, 0)        // 扩展切片copy(s[index+1:], s[index:]) // 将 index 位置后的元素整体右移s[index] = value        // 插入新值fmt.Println(s) // 输出: [1 2 99 3 4 5]
}

不管是哪种方法,都需要另外开辟空间

删除中间元素

删除中间元素就不会出现上述的问题,可以通过自身的性质来实现

二.底层原理

对于切片的底层原理,我们需要学习和熟记什么内容呢?

接下来做一个简单的总结:

  1. 首先是了解切片的结构,和数组有什么区别?
  2. 熟悉切片操作(增删改查,以及特殊的如何添加多个,中间添加,比较大小等等)
  3. 切片的扩容机制
  4. 切片的传参需要注意什么?
  5. nil 和 空 切片的区别是什么?

2.1 切片的底层结构

切片的底层结构

  • Pointer:指向实际存储数据的数组的指针。
  • Length:切片的当前长度,即切片中元素的数量。
  • Capacity:切片的容量,即从切片的起始位置到底层数组的末尾位置可以存储的元素数量。
type slice struct {array unsafe.Pointerlen   intcap   int
}

不难看出,这是一块连续的内存空间。

2.2 切片和数组的区别

  • 数组(Array)
  1. 固定长度: 数组是一个固定长度的序列,定义时需要指定其长度。例如,var arr [5]int 声明了一个包含 5 个整数的数组。
  2. 值类型: 数组是值类型,当传递数组给函数或赋值给另一个数组时,会进行一次完整的数组拷贝,包括所有元素。值传递。
  3. 大小不可变: 数组的长度在声明后无法更改。
  4. 内存分配: 数组通常在栈上分配内存。
  5. 基础数据类型: 数组属于基础数据类型,是一块连续的内存区域。

  • 切片(Slice)
  1. 动态长度: 切片是对数组的抽象,它可以动态增长和缩减,长度不固定。切片的长度是可以改变的。
  2. 引用类型: 切片是引用类型,它包含三个信息:指向底层数组的指针、切片的长度和切片的容量。引用传递。
  3. 灵活性: 切片可以视情况指向数组的一部分或整个数组。
  4. 传递效率: 切片作为引用类型传递时,不会复制底层数组,多个切片可以共享底层数组。
  5. 动态增长: 当向切片追加元素时,如果超出了切片的容量,Go 会重新分配更大的底层数组,容量变为原来的2倍。

2.3 切片的扩容机制

在之前的讲述过程中,并没有说到这个扩容机制,不过大家都肯定知道这个扩容机制,这一小节,就来具体了解一下扩容机制。

  • 首先扩容机制并不是单纯的按照倍数扩容,存在一个内存对齐机制。
  • 并且扩容机制是会开辟一个新的空间,回收旧的空间

先看一下这个扩容规则吧:

扩容有一个临界值,需要和这个值比较,从而选择不同的扩容。

接下来看下这个扩容机制

1.18之后,Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;其中cap为预期容量,也就是说添加了数据之后需要的容量

  • 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
  • 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
  • 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

为什么这样设置呢?

首先是双倍容量扩容的最大阈值从1024降为了256,只要超过了256,就开始进行缓慢的增长。其次是增长比例的调整,之前超过了阈值之后,基本为恒定的1.25倍增长,而现在超过了阈值之后,增长比例是会动态调整的,随着切片容量的变大,增长比例逐渐向着1.25进行靠拢

实际不然,还涉及内存对齐的机制

之所以要加入,是为了让扩容时不管内存大小,都比较平和,避免出现扩的太大,或者太少,少了会影响性能,因为容易导致多次扩容

在他的底层还设置的有67个跨度,也就是67个阈值,如果说我们规定的切片溢出进行扩容时,它首先按照他的规则进行扩容,但是他不是完全按照,比如他的阈值是48,你扩容之后他是40字节,他会把你默认变成48,从而做到一个内存对齐的操作。

举个例子吧

s3 := []int{1, 2}
s3 = append(s3, 3, 4, 5)
fmt.Println(cap(s3))

根据前文知,所需容量为5,又因所需容量大于2倍当前容量,故新容量也为5。

又因为int类型大小为8(等于64位平台上的指针大小),所以实际需要的内存大小为5 * 8 = 40字节。而67个跨度中最接近40字节的跨度为48字节,所以实际分配的内存容量为48字节。

最终计算真实的容量为48 / 8 = 6,和实际运行输出一致。这样就应该能很好的理解了

2.4 空切片和nil切片

在之前将数组的时候曾说过nil和空,这一届具体来说一下,所谓的空和nil到底是什么

type slice struct {array unsafe.Pointerlen   intcap   int
}

所谓的nil,其实就是指array这个指针指向是nil,也就是没有分配内存空间,如下图所示

也就是使用下面这种声明

var s []int // s 现在是 nil

对 nil 切片进行 len() 和 cap() 操作,结果都将返回 0。

你也可以将 nil 切片添加到任何切片的末尾,这等同于什么都没做。但是,与空切片相比,nil 切片在做 append() 操作时有一个重要的区别,那就是当 nil 切片通过 append() 函数追加元素后,它就会产生一个长度和容量都为1的新切片。

那什么又是空切片呢?

所谓的空其实是指这个arr数组是空的,但是这个指针有指向

s1 := []int{} // s1 是一个空切片,通过字面量创建
s2 := make([]int, 0) // s2 也是一个空切片,通过 make 创建

特别的是,对于一个空切片,即使紧接着进行 append() 操作,其地址也不会发生改变,因为切片在创建时就已经预分配了一定的内存空间。

总的来说,可以在需要表示“不存在”和“空集合”两种不同的状态时来区分 nil 切片和空切片。

除此之外,我们判断切片是否为空的时候,建议还是使用len比较好,不要使用nil去判断。

2.5 切片的传参

先说说为什么要收切片的传参,再有了对上述知识的接触,切片和数组都是属于浅拷贝,如果通过函数传参之后,修改这个传来的切片,会影响底层的数组嘛?

接下来我们就来探讨一下

package mainimport "fmt"func test(b []int) (c []int) {c = bc[2] = 999return
}func main() {a := []int{1, 2, 3, 4, 5}d := test(a)fmt.Println(a)fmt.Println(d)
}
//结果
//[1,2,999,4,5]
//[1,2,999,4,5]

运行完之后打印,会发现显然是会改变这个切片,即使你在函数里面赋值给其他变量,原因也很简单,就是因为他们公用了一个底层数组所导致的问题

如果你在传参修改切片,导致了扩容,又是一个怎么样的情况呢?

如果是这样的话,就会导致打印的数组内容不一样,原因是因为扩容会重新申请一个新的内存空间来存储它的扩容,而不是在原地址进行一个扩容

package mainimport "fmt"func test(b []int) []int {b = append(b, 999)return b
}func main() {a := []int{1, 2, 3, 4, 5}d := test(a)fmt.Println(a)fmt.Println(d)
}//[1 2 3 4 5]
//[1 2 3 4 5 999]

2.6 切片的深拷贝

切片的深拷贝是通过使用copy函数实现的,深拷贝和浅拷贝这里就不在展开讲述了。

通过copy生成的切片是一个全新的切片。

具体的内容在上述讲过,这里结合函数传参举一个例子:

package mainimport "fmt"func returnNewSliceUtils(s []int) []int {newSlice := make([]int, len(s))copy(newSlice, s)newSlice[0] = 0return newSlice
}
func returnSlice() {mySlice := []int{1, 2, 3}fmt.Println("调用函数之前的切片", mySlice)newSlice := returnNewSliceUtils(mySlice)fmt.Println("调用函数后的原切片", mySlice)fmt.Println("调用函数后的新切片:", newSlice)
}
func main() {returnSlice()
}结果:
调用函数之前的切片 [1 2 3]
调用函数后的原切片 [1 2 3]
调用函数后的新切片: [0 2 3]

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com