go slice 基本用法

slice(切片)是 go 里面非常常用的一种数据结构,它代表了一个变长的序列,序列中的每个元素都有相同的数据类型。
一个 slice 类型一般写作 []T,其中 T 代表 slice 中元素的类型;slice 的语法和数组很像,但是 slice 没有固定长度。

数组和切片的区别

数组有确定的长度,而切片的长度不固定,并且可以自动扩容。

数组的定义

go 中定义数组的方式有如下两种:

  1. 指定长度:
arr := [3]int{1, 2, 3}
  1. 不指定长度,由编译器推导出数组的长度:
arr := [...]{1, 2, 3}

上面这两种定义方式都定义了一个长度为 3 的数组。正如我们所见,长度是数组的一部分,定义数组的时候长度已经确定下来了

切片的定义

切片的定义方式跟数组很像,只不过定义切片的时候不用指定长度

s := []int{1, 2, 3}

在上面定义切片的代码中,我们可以看到其实跟数组唯一的区别就是少了个长度。
那其实我们可以把切片看作是一个无限长度的数组
当然,实际上它并不是无限的,它只是在切片容纳不下新的元素的时候,会自动进行扩容,从而可以容纳更多的元素。

数组和切片的相似之处

正如我们上面看到的那样,数组和切片两者其实非常相似,在实际使用中,它们也是有些类似的。

比如,通过下标来访问元素:

arr := [3]int{1, 2, 3}
// 通过下标访问
fmt.Println(arr[1]) // 2

s := []int{1, 2, 3}
// 通过下标访问
fmt.Println(s[1]) // 2

数组的局限

我们知道了,数组的长度是固定的,这也就意味着如果我们想往数组里面增加一个元素会比较麻烦,
我们需要新建一个更大的数组,然后将旧的数据复制过去,然后将新的元素写进去,如:

// 往数组 arr 增加一个元素:4
arr := [3]int{1, 2, 3}
// 新建一个更大容量的数组
var arr1 [4]int
// 复制旧数组的数据
for i := 0; i < len(arr); i++ {
    arr1[i] = arr[i]
}
// 加入新的元素:4
arr1[3] = 4
fmt.Println(arr1)

这样一来就非常的繁琐,如果我们使用切片,就可以省去这些步骤:

// 定义一个长度为 3 的数组
arr := [3]int{1, 2, 3}

// 从数组创建一个切片
s := arr[:]
// 增加一个元素
s = append(s, 4)
fmt.Println(s)

因为数组固定长度的缺点,实际使用中切片会使用得更加普遍。

重新理解 slice

在开始之前,我们来看看 slice 这个单词的意思:作为名词,slice 的意思有 片;部分;(切下的食物)薄片;,作为动词,slice 的意思有 切;把…切成(薄)片; 的意思。
从这个角度出发,我们可以把 slice 理解为从某个数组上 切下来的一部分(从这个角度看,slice 这个命名非常的形象)。我们可以看看下图:

在这里插入图片描述

在这个图中,A 是一个保存了数字 1~7sliceB 是从 A切下来的一部分,而 B 只包含了 A 中的一部分数据。
我们可以把 B 理解为 A 的一个 视图B 中的数据是 A 中的数据的一个 引用,而不是 A 中数据的一个 拷贝
(也就是说,我们修改 B 的时候,A 中的数据也会被修改,当然会有例外,那就是 B 发生扩容的时候,再去修改 B 的话就影响不了 A 了)。

slice 的内存布局

现在假设我们有如下代码:

// 创建一个切片,长度为 3,容量为 7
var s = make([]int, 3, 7)
s[0] = 1
s[1] = 2
s[2] = 3
fmt.Println(s)

对应的内存布局如下:

在这里插入图片描述

说明:

  • slice 底层其实也是数组,但是除了数组之外,还有两个字段记录切片的长度和容量,分别是 lencap
  • 上图中,slice 中的 array 就是切片的底层数组,因为它的长度不是固定的,所以使用了指针来保存,指向了另外一片内存区域。
  • len 表明了切片的长度,切片的长度也就是我们可以操作的下标,上面的切片长度为 3,这也就意味着我们切片可以操作的下标范围是 0~2。超出这个范围的下标会报错。
  • cap 表明了切片的容量,也就是切片扩容之前可以容纳的元素个数

切片容量存在的意义

对于我们日常开发来说,slice 的容量其实大多数时候不是我们需要关注的点,而且由于容量的存在,也给开发者带来了一定的困惑。
那么容量存在的意义是什么呢?意义就在于避免内存的频繁分配带来的性能下降(容量也就是提前分配的内存大小)。

比如,假如我们有一个切片,然后我们知道需要往它里面存放 1w 个元素,
如果我们不指定容量的话,那么切片就会在它存放不下新的元素的时候进行扩容,
这样一来,可能在我们存放这 1w 个元素的时候需要进行多次扩容,
这也就意味着需要进行多次的内存分配。这样就会影响应用的性能。

我们可以通过下面的例子来简单了解一下:

// Benchmark1-20    	100000000	        11.68 ns/op
func Benchmark1(b *testing.B) {
	var s []int
	for i := 0; i < b.N; i++ {
		s = append(s, 1)
	}
}

// Benchmark2-20    	134283985	         7.482 ns/op
func Benchmark2(b *testing.B) {
	var s []int = make([]int, 10, 100000000)
	for i := 0; i < b.N; i++ {
		s = append(s, 1)
	}
}

在第一个例子中,没有给 slice 设置容量,这样它就只会在切片容纳不下新元素的时候才会进行扩容,这样就会需要进行多次扩容。
而第二个例子中,我们先给 slice 设置了一个足够大的容量,那么它就不需要进行频繁扩容了。

最终我们发现,在给切片提前设置容量的情况下,会有一定的性能提升。

切片常用操作

创建切片

我们可以从数组或切片生成新的切片:

注意:生成的切片不包含 end

target[start:end]

说明:

  • target 表示目标数组或者切片
  • start 对应目标对象的起始索引(包含)
  • end 对应目标对象的结束索引(不包含)

如:

s := []int{1, 2, 3}
s1 := s[1:2]    // 包含下标 1,不包含下标 2
fmt.Println(s1) // [2]

arr := [3]int{1, 2, 3}
s2 := arr[1:2]
fmt.Println(s2) // [2]

在这种初始化方式中,我们可以省略 start

arr := [3]int{1, 2, 3}
fmt.Println(arr[:2]) // [1, 2]

省略 start 的情况下,就是从 target 的第一个元素开始。

我们也可以省略 end

arr := [3]int{1, 2, 3}
fmt.Println(arr[1:]) // [2, 3]

省略 end 的情况下,就是从 start 索引处的元素开始直到 target 的最后一个元素处。

除此之外,我们还可以指定新的切片的容量,通过如下这种方式:

target[start:end:cap]

例子:

arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s := arr[1:4:5]
fmt.Println(s, len(s), cap(s)) // [2 3 4] 3 4

往切片中添加元素

我们前面说过了,如果我们想往数组里面增加元素,那么我们必须开辟新的内存,将旧的数组复制过去,然后才能将新的元素加入进去。

但是切片就相对简单,我们可以使用 append 这个内置函数来往切片中加入新的元素:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1,2,3}...) // 追加一个切片

切片复制

go 有一个内置函数 copy 可以将一个切片的内容复制到另外一个切片中:

copy(dst, src []int)

第一个参数 dst 是目标切片,第二个参数 src 是源切片,调用 copy 的时候会把 src 的内容复制到 dst 中。

示例:

var a []int
var b []int = []int{1, 2, 3}

// a 的容量为 0,容纳不下任何元素
copy(a, b)
fmt.Println(a) // []

a = make([]int, 3, 3) // 给 a 分配内存
copy(a, b)
fmt.Println(a) // [1 2 3]

需要注意的是,如果 dst 的长度比 src 的长度小,那么只会截取 src 的前面一部分。

从切片删除元素

虽然我们往切片追加元素的操作挺方便的,但是要从切片删除元素就相对麻烦一些了。go 语言本身没有提供从切片删除元素的方法。
如果我们要删除切片中的元素,只有构建出一个新的切片:

在这里插入图片描述

对应代码:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
fmt.Println(a) // [1 2 3 4 5 6 7]

var b []int
b = append(b, a[:2]...) // [1 2]
b = append(b, a[5:]...) // [1 2 6 7]
fmt.Println(b) // [1 2 6 7]

在这个例子中,我们想从 a 中删除 3、4、5 这三个元素,也就是下标 2~4 的元素,
我们的做法是,新建了一个新的切片,然后将 3 前面的元素加入到这个新的切片中,
再将 5 后面的元素加入到这个新切片中。

最终得到的切片就是删除了 3、4、5 三个元素之后的切片了。

切片的容量到底是多少?

假设我们有如下代码:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)

s1 := a[:3]
// [1 2 3] 3 7
fmt.Println(s1, len(s1), cap(s1))

s2 := a[4:6]
// [5 6] 2 3
fmt.Println(s2, len(s2), cap(s2))

s1s2 可以用下图表示:

在这里插入图片描述

  • s1 只能访问 array 的前三个元素,s2 只能访问 56 这两个元素。
  • s1 的容量是 7(底层数组的长度)
  • s2 的容量是 3,从 5 所在的索引处直到底层数组的末尾。

对于 s1s2,我们都没有指定它的容量,但是我们打印发现它们都有容量,
其实在切片中,我们从切片中生成一个新的切片的时候,如果我们不指定容量,
那新切片的容量就是 s[start:end] 中的 start 直到底层数组的最后一个元素的长度。

切片可以共享底层数组

切片最需要注意的点是,当我们从一个切片中创建新的切片的时候,两者会共享同一个底层数组,
如上图的那样,s1s2 都引用了同一个底层的数组不同的索引,
s1 引用了底层数组的 0~2 下标范围,s2 引用了底层数组 4~5 下标范围。

这意味着,当我们修改 s1s2 的时候,原来的切片 a 也会发生改变:

var a = make([]int, 7, 7)
for i := 0; i < 7; i++ {
    a[i] = i + 1
}
// [1 2 3 4 5 6 7]
fmt.Println(a)

s1 := a[:3]
// [1 2 3]
fmt.Println(s1)

s1[1] = 100
// [1 100 3 4 5 6 7]
fmt.Println(a)
// [1 100 3]
fmt.Println(s1)

在上面的例子中,s1 这个切片引用了和 a 一样的底层数组,
然后在我们修改 s1 的时候,a 也发生了改变。

切片扩容不会影响原切片

上一小节我们说了,切片可以共享底层数组。但是如果切片扩容的话,那就是一个全新的切片了

var a = []int{1, 2, 3}
// [1 2 3] 3 3
fmt.Println(a, len(a), cap(a))

// a 容纳不下新的元素了,会进行扩容
b := append(a, 4)
// [1 2 3 4] 4 6
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 2 3]
fmt.Println(a)
// [1 100 3 4]
fmt.Println(b)

在上面这个例子中,a 是一个长度和容量都是 3 的切片,这也就意味着,这个切片已经满了。
在这种情况下,我们再往其中追加元素的时候,就会进行扩容,生成一个新的切片
因此,我们可以看到,我们修改了 b 的时候,并没有影响到 a

下面的例子就不一样了:

// 长度为 2,容量为 3
var a = make([]int, 2, 3)
a[0] = 1
a[1] = 2
// [1 2] 2 3
fmt.Println(a, len(a), cap(a))

// a 还可以容纳新的元素,不用扩容
b := append(a, 4)
// [1 2 4] 3 3
fmt.Println(b, len(b), cap(b))
b[1] = 100
// [1 100]
fmt.Println(a)
// [1 100 4]
fmt.Println(b)

在后面这个例子中,我们只是简单地改了一下 a 初始化的方式,改成了只放入两个元素,但是容量还是 3
在这种情况下,a 可以再容纳一个元素,这样在 b := append(a, 4) 的时候,创建的 b 底层的数组其实跟 a 的底层数组依然是一样的。

所以,我们需要尤其注意代码中作为切片的函数参数,如果我们希望在被调函数中修改了切片之后,在 caller 里面也能看到效果的话,最好是传递指针

func test1(s []int) {
	s = append(s, 4)
}

func test2(s *[]int) {
	*s = append(*s, 4)
}

func TestSlice(t *testing.T) {
	var a = []int{1, 2, 3}
	// [1 2 3] 3 3
	fmt.Println(a, len(a), cap(a))
	test1(a)
	// [1 2 3] 3 3
	fmt.Println(a, len(a), cap(a))

	var b = []int{1, 2, 3}
	// [1 2 3] 3 3
	fmt.Println(b, len(b), cap(b))
	test2(&b)
	// [1 2 3 4] 4 6
	fmt.Println(b, len(b), cap(b))
}

在上面的例子中,test1 接收的是值参数,所以在 test1 中切片发生扩容的时候,TestSlice 里面的 a 还是没有发生改变。
test2 接收的是指针参数,所以在 test2 中发生切片扩容的时候,TestSlice 里面的 b 也发生了改变。

总结

  • 数组跟切片的使用上有点类似,但是数组代表的是有固定长度的数据序列,而切片代表的是没有固定长度的数据序列。
  • 数组的长度是类型的一部分,有两种定义数组的方式:[2]int{1, 2}[...]int{1, 2}
  • 数组跟切片都可以通过下标来访问其中的元素,可以访问的下标范围都是 0 ~ len(x)-1x 表示的是数组或者切片。
  • 数组无法追加新的元素,切片可以追加任意数量的元素。
  • slice 的数据结构里面包含了:array 底层数组指针、len 切片长度、cap 切片容量。
  • 创建切片的时候,指定一个合适的容量可以减少内存分配的次数,从而在一定程度上提高程序性能。
  • 我们可以从数组或者切片创建一个新的切片:array[1:3] 或者 slice[1:3]
  • 使用 append 内置函数可以往切片中添加新的元素。
  • 使用 copy 内置函数可以将一个切片的内容复制到另外一个切片中。
  • 切片删除元素没有好的办法,只能截取被删除元素前后的数据,然后复制到一个新的切片中。
  • 假设我们通过 slice[start:end] 的方式从切片中创建一个新的切片,那么这个新的切片的容量是 cap(slice) - start,也就是,从 start 到底层数组最后一个元素的长度。
  • 使用切片的时候需要注意:切片之间会共享底层数组,其中一个切片修改了切片的元素的时候,也会反映到其他切片上。
  • 函数调用的时候,如果被调函数内发生扩容,调用者是无法知道的。如果我们不想错过在被调函数内切片的变化,我们可以传递指针作为参数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/351237.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一款强大的矢量图形设计软件:Adobe Illustrator 2023 (AI2023)软件介绍

Adobe Illustrator 2023 (AI2023) 是一款强大的矢量图形设计软件&#xff0c;为设计师提供了无限创意和畅行无阻的设计体验。AI2023具备丰富的功能和工具&#xff0c;让用户可以轻松创建精美的矢量图形、插图、徽标和其他设计作品。 AI2023在界面和用户体验方面进行了全面升级…

python-自动化篇-运维-监控-简单实例-道出如何使⽤Python进⾏网络监控?

如何使⽤Python进⾏⽹络监控&#xff1f; 使⽤Python进⾏⽹络监控可以帮助实时监视⽹络设备、流量和服务的状态&#xff0c;以便及时识别和解决问题。 以下是⼀般步骤&#xff0c;说明如何使⽤Python进⾏⽹络监控&#xff1a; 选择监控⼯具和库&#xff1a;选择适合⽹络监控需…

网络防御——NET实验

一、实验拓扑 二、实验要求 1、生产区在工作时间&#xff08;9&#xff1a;00---18&#xff1a;00&#xff09;内可以访问服务区&#xff0c;仅可以访问http服务器&#xff1b; 2、办公区全天可以访问服务器区&#xff0c;其中&#xff0c;10.0.2.20可以访问FTP服务器和HTTP服…

OSI七层模型 | TCP/IP模型 | 网络和操作系统的联系 | 网络通信的宏观流程

文章目录 1.OSI七层模型2.TCP/IP五层(或四层)模型3.网络通信的宏观流程3.1.同网段通信3.2.跨网段通信 1.OSI七层模型 在计算机通信诞生之初&#xff0c;不同的厂商都生产自己的设备&#xff0c;都有自己的网络通讯标准&#xff0c;导致了不同厂家之间各种协议不兼容&#xff0…

Oracle篇—分区索引的重建和管理(第三篇,总共五篇)

☘️博主介绍☘️&#xff1a; ✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ ✌✌️擅长Oracle、MySQL、SQLserver、Linux&#xff0c;也在积极的扩展IT方向的其他知识面✌✌️ ❣️❣️❣️大佬们都喜欢静静的看文章&#xff0c;并且也会默默的点赞收藏加关注❣…

Web3创业:去中心化初创公司的崛起

随着Web3时代的到来&#xff0c;去中心化技术的崛起不仅令人瞩目&#xff0c;也为创业者带来了前所未有的机遇。在这个新的时代&#xff0c;一批去中心化初创公司正崭露头角&#xff0c;重新定义着商业和创新的边界。本文将深入探讨Web3创业的趋势&#xff0c;以及去中心化初创…

VScode 好用的插件合集

VS Code是一个轻量级但功能强大的源代码编辑器&#xff0c;轻量级指的是下载下来的VS Code其实就是一个简单的编辑器&#xff0c;强大指的是支持多种语言的环境插件拓展&#xff0c;也正是因为这种支持插件式安装环境开发让VS Code成为了开发语言工具中的霸主&#xff0c;让其同…

策略者模式-C#实现

该实例基于WPF实现&#xff0c;直接上代码&#xff0c;下面为三层架构的代码。 目录 一 Model 二 View 三 ViewModel 一 Model using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace 设计模式练…

Facebook 广告帐户:多账号运营如何防止封号?

Facebook目前是全球最受欢迎的社交媒体平台之一&#xff0c;拥有超过27亿活跃用户。因此&#xff0c;它已成为个人和企业向全球受众宣传其产品和服务的重要平台。 然而&#xff0c;Facebook 制定了广告商必须遵守的严格政策和准则&#xff0c;以确保其广告的质量和相关性&…

vulnhub靶场之Five86-1

由于这些文章都是从我的hexo博客上面复制下来的&#xff0c;所以有的图片可能不是很完整&#xff0c;但是不受影响&#xff0c;如果有疑问&#xff0c;可以在评论区留言&#xff0c;我看到之后会回复。 一.环境搭建 1.靶场描述 Five86-1 is another purposely built vulnerab…

Vue2:通过代理服务器解决跨域问题

一、场景描述 现在的项目大多数是前后端分离的。Vue前端项目通过ajax去请求后端接口的时候&#xff0c;会有同源策略的限制。从而产生跨域问题。 二、基本概念 1、什么是同源策略&#xff1f; 就是前端服务和后端服务的协议名&#xff0c;IP或主机名&#xff0c;端口号不完…

达梦数据库——记录一次离谱的登录失败报错

好久没更新了哇 前面有整理过一些常见的数据库登录失败问题哈&#xff0c;今天记录一个遇到概率比较小&#xff0c;但碰上了一般不太容易找到原因的登录失败问题。 今天给客户同时初始化了三台服务器数据库&#xff0c;惟独这一台死活登不进去&#xff0c;满脑子问号&#xf…

k8s实例

k8s实例举例 &#xff08;1&#xff09;Kubernetes 区域可采用 Kubeadm 方式进行安装。 &#xff08;2&#xff09;要求在 Kubernetes 环境中&#xff0c;通过yaml文件的方式&#xff0c;创建2个Nginx Pod分别放置在两个不同的节点上&#xff0c;Pod使用动态PV类型的存储卷挂载…

【华为 ICT HCIA eNSP 习题汇总】——题目集10

1、以下哪个动态路由协议不能应用在 IPv6 网络中&#xff1f; A、IS-IS B、RIPng C、BGP4 D、OSPFv3 考点&#xff1a;路由技术原理 解析&#xff1a;&#xff08;A&#xff09; IS-ISv6 是在 IPv6 环境下&#xff0c;IS-IS 协议进行了相应的扩展和改进&#xff0c;以适应 IPv6…

Go语言grpc服务开发——Protocol Buffer

文章目录 一、Protocol Buffer简介二、Protocol Buffer编译器安装三、proto3语言指南四、序列化与反序列化五、引入grpc-gateway1、插件安装2、定义proto文件3、生成go文件4、实现Service服务5、gRPC服务启动方法6、gateway服务启动方法7、main函数启动8、验证 相关参考链接&am…

【AI大模型应用开发】1.2 Prompt Engineering(提示词工程)- 站在巨人的肩膀上,超实用!常用提示词整理

通过上两篇文章我们学习和实践了Prompt的书写要素、原则与技巧&#xff0c;以及了解了一些进阶的优化方法。 本来今天是想收集一些网上比较好的Prompt提示词&#xff0c;来与大家共同学习下别人的书写方式&#xff0c;吸取别人的经验&#xff0c;对Prompt有个更深入的理解。 但…

武忠祥2025高等数学,基础阶段的百度网盘+视频及PDF

考研数学武忠祥基础主要学习以下几个方面的内容&#xff1a; 1.微积分:主要包括极限、连续、导数、积分等概念&#xff0c;以及它们的基本性质和运算方法。 2.线性代数:主要包括向量、向量空间、线性方程组、矩阵、行列式、特征值和特征向量等概念&#xff0c;以及它们的基本…

第十七讲_HarmonyOS应用开发Stage模型应用组件

HarmonyOS应用开发Stage模型应用组件 1. 应用级配置2. Module级配置3. Stage模型的组件3.1 AbilityStage3.1.1 AbilityStage的创建和配置3.1.2 AbilityStage的生命周期回调3.1.3 AbilityStage的事件回调&#xff1a; 3.2 UIAbility3.2.1 UIAbility生命周期3.2.3 UIAbility启动模…

pinia实现todos

store/todos.js //导入defineStore import {defineStore} from pinia const userTodosStoredefineStore(todos,{ state:()>({// list:[// {id:1,name:吃饭,done:false},// {id:2,name:睡觉,done:true},// {id:3,name:打豆豆,done:false}// ],list:JSON.parse(l…

零基础学编程入门视频教程,中文编程轻松学

零基础学编程入门视频教程&#xff0c;中文编程轻松学 一、前言 不论是正在学习编程的大学生&#xff0c;还是IT人士或者是编程爱好者&#xff0c;在学习编程的过程中用正确的学习方法可以达到事半功倍的效果。对于初学者&#xff0c;可以先从中文编程入手&#xff0c;学习编…