css slice后的长度_听说你还搞不懂Golang的Slice?看这一篇就够了!

在前面的文章中,我和大家一起学习了一下关于 Go 语言中数组的知识,当时有提到过一个知识点:在函数中传递数组是非常耗资源的一件事,所以更推荐大家使用切片(slice)来这么做。

那么切片又是一个怎样的东西呢?看完这篇文章你就知道了!

上一篇传送门:Go by Example-图解数组

初识切片

切片(slice)也是一种数据结构,它和数组非常相似,但它是围绕动态数组的概念设计的,可以按需自动改变大小。

切片的动态增长是通过内置函数 append 来实现的,这个函数可以快速且高效地增长切片。

使用切片后,不仅可以更方便地管理和使用数据集合,还可以在切片的基础上继续使用切片来缩小一个切片的长度范围。

因为切片的底层就是一个数组,所以切片和数组的一些操作类似。比如:获得切片索引、迭代切片等。

切片的对象非常小,它是一个只有 3 个字段的数据结构,分别是:

1.指向底层数组的指针2.切片的长度3.切片的容量

了解了一下切片的好处和特性之后,我们再来看看如何创建切片吧。

创建切片

创建切片的方式有两种,一种是使用内置的 make 函数,一种是使用切片的字面量。

下面我们来看一下这两种创建方式的使用方法。

使用 make 创建

在创建之前我们先简单地了解一下 make 函数的作用。

make 只能应用于三种数据类型:本文中的 slice、以及后面要说的 map 和 chan。

make 会为它们分配内存、初始化一个对应类型的对象并返回。注意,返回值的类型依然是被 make 的那个类型,make 只对其做了一个引用。

make 主要是用来做初始化的,记住这点,这在后面的学习中非常重要(但不是本章的后面)。

使用长度和容量声明整型切片
slice := make([]int, 4, 5)

在使用 make 创建切片的时候,一共可以传入三个参数:

1.声明一个 int 类型的切片2.指定切片的长度3.指定切片的容量,也就是底层数组的长度

由于切片可以按需改变大小,所以在声明类型的时候并不需要指定长度(即[123]int 这种写法),也不需要让编译器自己去推断数组的大小(即[...]int 这种写法)。

所谓的容量其实就是切片可以增加到的最大长度。如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。

长度和容量一样的情况

如果你在使用 make 函数创建切片的时候只使用了两个参数的话,那么这个切片的长度和容量将会是一样的。

例如这里我声明了一个 string 类型的切片,并将它的长度和容量都设置为 6。

slice := make([]string, 6)
注意:切片的容量不能小于其长度
slice: = make([]int, 3, 1)

这行代码在编译的时候是会报错的,因为切片的容量不能小于长度,需要注意。

通过字面量创建

// 创建一个字符串切片并赋值,其长度和容量都是5。slice := []string {"I", "Love", "My", "祖", "国"}// 创建一个整型切片并赋值,其长度和容量都是3。slice := []int {10, 20, 30}

另外,在使用切片字面量时,我们可以设置初始长度和容量。

// 创建一个整形切片,使用整形数字 8 初始化第 10 个元素slice := []int {9: 8}// 注:上面的这个 9 代表下标 9,也就是第 10 个元素

上面的代码表示声明一个长度和容量都为 10 的数组,并把第 10 个元素的值设定为 8,其他位置的元素此时因为没有赋值,所以是对应类型的零值。这里的话因为声明类型是 int 的关系,所以是一个 int 值的 0。

用哪种方式创建呢?

这个主要得看在声明切片的时候,是否知道里面部分元素的值。

如果不知道的话,不管使用哪种方式都可以。

但如果想在声明的时候顺带给元素赋值,那么就可以选择使用字面量的方式。

为什么切片会和底层数组有关系呢?

这个实际上和它的数据结构声明是有关系的,切片实际上是一个结构体类型的数据结构,看一下切片类型的源码就知道了:

type slice struct {    array unsafe.Pointer // 底层数组    len int    cap int}

不过由于本文主要说的是切片的关系,结构体等内容就先暂且不谈了,现在我们只需要知道切片底层有个数组就可以了。

图解切片结构

下面通过一张图来对切片做一个直观的理解,就拿下面这个例子来说:

slice := make([]int, 4, 5)slice[2] = 9

把它画成图的话就是这个样子的:

83a4395a769bd720c606bf5662e6d5c8.png

因为切片存的只是指向底层数组的指针而已,所以切片占用的内寸空间是很小的。

我们来做个简单的计算,我的电脑是 64 位的,unsafe.Pointer 类型变量占用 8 个字节,int 类型变量占用 8 个字节,所以算起来这个切片所占的内存空间大小只有 8 + 8 * 2 = 24 字节,非常小。

操作切片

通过切片创建切片

除了上面的方式以外,我们还可以使用切片来创建切片。

// 创建一个整型切片,其长度和容量都是 5 个元素slice := []int{1, 2, 3, 4, 5}// 创建一个新切片,其长度为 2 个元素,容量为 4 个元素newSlice := slice[1:3]

执行完这段操作以后,我们就有两个切片了,它们共享一个底层数组,我们通过一个图来帮助理解一下这个过程。

fbb8cbb6f5502de11721c46bb7db52eb.png

新切片的下标 [0] 对应的实际是底层数组的下标 [1]

如果没有限定容量的大小,那么可以得知:

•新切片的长度为 3-1=2。•新切片的容量为 5-1=4。(这个5代表原来切片总长度)

说到容量,还需要注意一点,它一般只是用来增加切片长度用的,我们无法通过下标去取里面的内容。

例如:

package mainimport "fmt"func main () {    slice := make([]int, 4, 5)    newSlice := slice[1:3]    fmt.Println(newSlice[1])    fmt.Println(newSlice[2])}

运行结果会是:

panic: runtime error: index out of range

使用切片创建新的切片的时候,实际上一共是有三个参数的,前面我们已经使用了两个,现在我们来看看第三个参数。

第三个参数是用来限定新的切片的最大容量的,这个最大容量计算是从索引位置开始,加上希望容量中包含的元素的个数得到的。

举个例子:

// 创建一个整型切片,其长度和容量都是 5 个元素slice := []int{1, 2, 3, 4, 5}// 创建一个新切片,其长度为 3-1=2 个元素,容量为 4-1=3 个元素newSlice := slice[1:3:4]

这里我们要获取的新切片要求是从底层数组索引 1 的位置开始,然后取其后面最多 3 个数。

执行后得到的就是我们要写的第三个参数:4。

此时如果用图表示的话是这个样子:

2ab94ed09d1a4ef55930a1d1669b9b1b.png

灰色部分是新切片不能拓展到的部分,原因很简单,因为我们把最大容量设置为了 3。

注意:如果设置的容量比可用的容量还大,就会得到一个语言运行时错误。

nil 切片

在声明切片时不做任何初始化,就会创建一个 nil 切片,nil 切片可以用于很多标准库和内置函数。

// 创建 nil 整型切片var slice [] int

nil 切片长度为 0,容量也为 0。

那么这个东西有什么用呢?

比如说我们调用了一个函数,我们希望它返回一个切片,但是运行期间发生了异常,这个时候我们就可以通过判断结果是否为 nil,得知程序是否出现异常了。

切片赋值

对切片赋值就很简单了,直接通过它的下标进行赋值即可。举个例子:

slice := []int{10, 20, 30}slice[1] = 3

得到结果如下:

[10,3,30]

注意:赋值的时候使用的索引,不能超过切片的最大索引,也就是切片的长度 - 1。

如果你需要给通过切片创建的切片进行赋值,那么你需要注意了!前面说过新旧切片是共享同一个底层数组的,而修改切片的值实际上是在修改底层数组的值,所以这就产生了一个问题,如果对新的切片赋值,那么旧的切片的值也会发生变化。

举个例子:

package mainimport "fmt"func main () {    slice := make ([]int, 4, 5)    newSlice := slice[1:3]    fmt.Printf("旧切片赋值之前:% d\n", newSlice)    fmt.Printf("新切片赋值之前:% d\n", slice)    newSlice[1] = 666    fmt.Printf("旧切片赋值之前:% d\n", slice)    fmt.Printf("新切片赋值之前:% d\n", newSlice)}

得到结果如下:

新切片赋值之前:[0 0]旧切片赋值之前:[0 0 0 0]新切片赋值之后:[0 0 666 0]旧切片赋值之后:[0 666]

看到了吗?旧切片的值也被改变了!那么同理,如果对旧切片赋值,新切片也是会发生变化,这个就不做演示了,和上面这个结果类似。

切片增长

Go 语言内置的 append 函数可以增加切片的长度,使用 append 需要一个被操作的切片和一个要追加的值。

append 必定会增加新切片的长度,而切片容量的变化则取决于被操作的切片的可用容量,可增长可不增长。

简单来说,如果 append 操作完之后,切片内的元素个数不大于容量数,那么新的切片就不增加容量。同样,此时的新切片还是和之前的切片共享同一个底层数组。但是这种做法我并不建议你使用!

举个例子:

package mainimport "fmt"func main() {    slice := []int{1,2,3,4,5}    newSlice := slice[1:3:4]    fmt.Printf("新切片之前元素:% d\n", newSlice)    fmt.Printf("旧切片之前元素:% d\n", slice)    newSlice = append(newSlice, 6)    fmt.Printf("旧切片增加之后元素:% d\n", slice)    fmt.Printf("新切片此时元素:% d\n", newSlice)

得到结果如下:

新切片赋值之前:[2 3]旧切片赋值之前:[1 2 3 4 5]旧切片增加之后元素:[1 2 3 6 5]新切片此时元素:[2 3 6]

让我们通过下面这张图来分析一下,上面都发生了什么。

c9aca4838a7ef80dc548f89a36cb31f1.png

看图可以发现,我们在增加新切片的元素的时候,无意中修改了底层的数组元素,这导致旧的切片的值也发生了变化,所以我更推荐你使用下面这种方式对新的切片进行元素的增加:

还记得前面说的:“使用切片创建切片的时候,可以使用第三个参数限制其最大容量”么?

这里我们要说的就是使用 append 之后,新的切片容量增大的情况。

如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,然后将被引用的现有的值复制到新数组里,再追加新的值。

也就是说,这种情况新的切片可以单独享用底层的数组,这样的话即使你修改了新切片的值,对旧的切片也不会造成任何影响,因为它们不再共享同一个底层数组。

举个例子:

slice := []int{1, 2, 3, 4, 6}newSlice := append(slice, 6)
02ce7750536873cac6d29b42cd28a229.png 当这个 append 操作完成后,newSlice 会拥有一个全新的底层数组,这个数组的容量是原来的两倍。

函数 append 会智能地处理底层数组的容量增长,在切片的容量小于 1000 个元素时,它总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长倍数就会被设为 1.25,也就是说会每次增加 25% 的容量。

append 除了可以添加元素以外,还可以在一个切片中追加另一个切片。只需要通过 ... 来将第二个切片的元素做一个拆分就行了,这里的 ... 就类似于 Python 里的解包操作。

s1 := []int {1, 2}s2 := []int {3, 4}fmt.Printf("% v\n", append(s1, s2...))输出:[1 2 3 4]

切片迭代

对了,切片还可以像数组那样去迭代,只需要这样就行了:

slice := []int {1, 2, 3, 4}for index, value := range slice {    fmt.Printf("Index: % d Value: % d\n", index, value)}

在函数间传递切片

切片在函数间进行传递的时候,只是复制了切片的本身,不会涉及底层的数组。

前面我们计算了一下,在 64 位的机器上,切片仅占用了 24 个字节,而在函数间传递 24 字节的数据是非常简单、快速的事情。

这正是切片效率高的地方,不需要传递指针,也不需要处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本就可以了,非常的简单快捷。

参考资料

•https://www.jb51.net/article/126703.htm•https://www.cnblogs.com/chenpingzhao/p/9918062.html•https://blog.csdn.net/qq_19018277/article/details/100578553•http://www.meirixz.com/archives/80658.html


文章作者:「夜幕团队 NightTeam」 - 陈祥安

润色、校对:「夜幕团队 NightTeam」 - Loco

夜幕团队成立于 2019 年,团队包括崔庆才、周子淇、陈祥安、唐轶飞、冯威、蔡晋、戴煌金、张冶青和韦世东。

涉猎的编程语言包括但不限于 Python、Rust、C++、Go,领域涵盖爬虫、深度学习、服务研发、对象存储等。团队非正亦非邪,只做认为对的事情,请大家小心。

dcd80bbc1f2eae4df9b59e28eea029dd.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值