当前位置: 首页 > news >正文

Go 语言 slice(切片) 的使用

序言

 在许多开发语言中,动态数组是必不可少的一个组成部分。在实际的开发中很少会使用到数组,因为对于数组的大小大多数情况下我们是不能事先就确定好的,所以他不够灵活。动态数组通过提供自动扩容的机制,极大地提升了开发效率。这篇文章将介绍 Go 语言中的动态数组 — slice(切片)


1. 数据结构

 切片的组成如下, 每一个字段的含义如下:

  • Data:指向存储元素数组的指针;
  • Len:该数组中元素的个数
  • Cap:该数组的容量大小
type SliceHeader struct {Data uintptrLen  intCap  int
}

如果你之前了解过 C++ 中的 vector 你会发现其实他们的思路是一样的。一个实际存储元素的切片如下:在这里插入图片描述


2. 切片的初始化

a. 声明但不初始化

 在 Go 语言中,如果你声明一个切片但不初始化它,它的默认值是 nil。这意味着该切片没有指向任何底层数组,长度和容量都为 0:

func main() {var slice []intsliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))dataPtr := unsafe.Pointer(sliceHeader.Data)fmt.Printf("data = %v, len = %d, cap = %d\n", dataPtr, len(slice), cap(slice))
}

这里程序的输出是:

data = , len = 0, cap = 0

b. 带初始值初始化

 比起第一种方式,这个会在声明的时候带上字面值来初始化一个切片:

slice := []int{1, 2, 3} 
fmt.Printf("data = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))

此时,该切片的 lencap 会和元素数量保持一致,程序输出:

data = [1 2 3], len = 3, cap = 3

c. 使用 make 初始化

 使用 make 来初始化一个切片也有两者方式,首先是第一种:

slice := make([]int, 5)
fmt.Printf("data = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))

这代表创建一个切片,并且切片的 lencap 都是 5,切片的元素的值采用该类型的默认值:

data = [0 0 0 0 0], len = 5, cap = 5

第二种是将 lencap 分别赋值:

slice := make([]int, 2, 4)
fmt.Printf("data = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))

这代表创建一个切片,并且切片的len 是 2,cap 是 4:

data = [0 0], len = 2, cap = 4

这也是最常用的方式,使用 make 来预先分配内存大小可以避免后续添加元素时频繁进行扩容操作!

d. 下标索引初始化

Go 支持指定一个索引范围来初始化一个切片,这是 C++vector 所不具备的能力,举个例子:

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

这里有一个长度为 5 的数组,现在使用索引范围 [1, 4) 「左闭右开」 来初始化一个切片,甚至还可以这样表达:

slice = arr[:4]   // 等价于 [0:4]
slice = arr[1:]   // 等价于 [1:len(arr) - 1]

现在有一个问题,使用索引初始化的切片和原数组是什么关系呢?换句话说这里是否涉及到了深拷贝呢?上代码:

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // [2, 3, 4]
slice[0] = 0 // 修改值
fmt.Printf(“arr=%v\n”, arr)
fmt.Printf(“slice=%v\n”, slice)

输出结果是:

arr=[1 0 3 4 5]
slice=[0 3 4]

上文中我们了解到了一个 slice 的结构是怎么样的,结合输出的结果,不难推断出 data 指针指向了该数组的第二个位置,如下:
在这里插入图片描述
这里 cap 的大小为什么是 4 怎么得到的呢 — cap = cap(arr) - 1


3. 切片的追加和扩容

a. 元素追加

 我们可以通过 append 操作来在切片最后追加元素,追加方式也有多种,举个栗子:

slice := []int{1, 2}
slice = append(slice, 3)       // 追加一个元素
slice = append(slice, 4, 5, 6) // 追加多个元素
slice = append(slice, []int{7, 8}...) // 追加一个切片,...表示解包,不能省略

对于追加的操作,大家是否存在疑惑的点呢?我刚开始就不理解为什么在追加操作后对 slice 进行赋值的操作。这是因为 append 函数有一个重要的特性需要特别注意:它可能会返回一个新的底层数组(取决于是否进行扩容操作)。如果没有进行赋值操作,那么 slice 还是指向原来的数组,举个栗子:

在这里插入图片描述

b. 切片扩容

 当切片的 len 等于 cap 时,在下一次 append 操作前就会进行一次扩容操作,扩容的逻辑如下:

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

扩容的策略总结如下:
在这里插入图片描述
可以看到 Go 语言增长容量的策略还是比较缓和的。


4. 切片易踩的坑

a. 参数传递类型傻傻分不清

 首先,我们先聊聊 C++ 当中的值传递和引用传递,就比如:

int main() {vector<int> vec = { 1, 2, 3, 4, 5 }funcJustForRead(vec)return 0;
}void funcJustForRead(vector<int> &vec) {...
}

对于某些只读的场景,我们一般会传引用,这样就大大减少了拷贝带来的开销。在 Go 语言中好像并没有 引用 的概念?但是仔细思考一下,Go 真的需要吗:

func main() {slice := []int{ 1, 2, 3, 4, 5 }funcJustForRead(slice)
}func funcJustForRead(slice []int) {...
}

形参是实参的拷贝,slice 中指向元素的是 data 指针,即使形参和实参的 data 不一样,但是两者是指向的同一个数组,所以不需要引用。
 现在,这里有一个函数会对切片进行追加操作,我依然是值传递是否还是可行呢?举个栗子(假设这里不涉及扩容操作):

func main() {slice := []int{1, 2}fmt.Println(slice)funcForAppend(slice)fmt.Println(slice)
}func funcForAppend(slice []int) {slice = append(slice, 3)
}

输出结果是:

[1 2]
[1 2]

并没有预想的新增一个值,为什么?上面我们介绍了,append 会返回一个新的切片,我们在 main 中使用的还是原来的切片。怎么解决呢?传递指针:

func main() {slice := make([]int, 0, 2)fmt.Println(slice)funcForAppend(&slice)fmt.Println(slice)
}func funcForAppend(slice *[]int) {*slice = append(*slice, 3)
}

b. len 和 cap 傻傻分不清

 之前我们谈到过,可以预先分配好空间,可以避免后续的频繁扩容操作,但是是否会有以下的误解呢:

func main() {slice := make([]int, 5)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)fmt.Println(slice) // [0 0 0 0 0 1 1 1 1]
}

这里代表预先分配好 5 个空间,并且每一个空间使用该类型的默认值填充,当我们新加入元素时,是在已有的基础上往后添加而不是从前开始覆盖。正确的姿势应该是这样子的:

func main() {slice := make([]int, 0, 5)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)slice = append(slice, 1)fmt.Println(slice) // [1 1 1 1]
}

5. 总结

 不仅只是会使用,并且知其所以然。我自认为这是非常重要的,这不仅能够很大程度上减小我们在开发中犯错的概念,还能够有效提升代码的质量。所以通过这篇 silce 带我们走入 Go 的世界吧。

相关文章:

  • Git安装教程及常用命令
  • 数据结构-树(2)
  • [python] 面向对象的三大特性-封装及新式类
  • Python制作Dashboard【待续】
  • upload-labs通关笔记-第01关 文件上传之前端绕过(3种渗透方法)
  • Clinica集成化的开源平台-神经影像研究
  • 解决下拉框数据提交后回显名称不对
  • 企业级常用的 CentOS版本
  • 如何指定conda环境打包成docker镜像
  • 5G网络:能源管理的“智能电网“革命,Python如何成为关键推手?
  • 苹果电脑笔记本macos Mac安装mixly 米思齐软件详细指南
  • OpenHarmony Linux内核本地管理
  • Matlab 模糊pid控制的永磁同步电机PMSM
  • 【Python】Python 单例模式 8 大核心应用场景深度解析(2025 新版)
  • 虚拟主机与独立服务器:哪个更好?
  • 无人机信号线被电磁干扰导致停机
  • RDD的五大特征
  • hdfs客户端操作-文件上传
  • 高速系统设计实例设计分析
  • Linux 系统中设置开机启动脚本
  • 牧原股份子公司与养殖户种猪买卖纠纷案一审胜诉
  • “不为一时一事所惑,不为风高浪急所扰”——习近平主席对俄罗斯进行国事访问并出席纪念苏联伟大卫国战争胜利80周年庆典纪实
  • 这座古村,藏着多少赣韵风华
  • 重庆荣昌出圈背后:把网络流量变成经济发展的增量
  • 身临其境感受伟人思想力量,“马克思书房”在上海社科馆揭幕
  • 首批证券公司科创债来了!拟发行规模超160亿元