当前位置: 首页 > 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 的世界吧。

http://www.xdnf.cn/news/401815.html

相关文章:

  • 智能停车场如何实现无缝通信?Canopen转 Profibus网关来解答
  • [高阶数据结构]二叉树经典面试题
  • 【秣厉科技】LabVIEW工具包——OpenCV 教程(21):CUDA 加速方案
  • 【生产实践】Linux中/usr/bin、/usr/sbin与/usr/local的关系解析(2025年技术规范)
  • 养生:拥抱健康生活的全方位指南
  • 多模态论文笔记——Coca
  • 基于Vue3.0的高德地图api教程005:实现绘制线并编辑功能
  • 一个例子看LLM的工具调用流程
  • js应用opencv
  • java每日精进 5.11【WebSocket】
  • Java后端文件类型检测(防伪造)
  • zuoyyyeee
  • 数据可视化:用一张图讲好一个故事
  • 安装Python和配置开发环境
  • 《 C++ 点滴漫谈: 三十七 》左值?右值?完美转发?C++ 引用的真相超乎你想象!
  • 创建三个网络,分别使用RIP、OSPF、静态,并每个网络10个电脑。使用DHCP分配IP
  • 第五十六篇 Java面向对象编程深度解析:构建高内聚低耦合的系统架构
  • Spring Boot中Redis序列化配置详解
  • 【美国将取消对能源之星支持 严重影响AI服务器】
  • 使用vite重构vue-cli的vue3项目
  • 基于粒子群算法的配电网重构
  • Kotlin与Qt跨平台框架深度解析:业务逻辑共享与多语言集成
  • MySQL-逻辑架构
  • python二手书交易管理系统
  • 如何调整yarn.nodemanager.vmem-pmem-ratio参数?
  • 什么是IP专线?企业数字化转型的关键网络基础设施
  • 阿里云人工智能大模型通义千问Qwen3开发部署
  • ASP.NET Core Identity框架使用指南
  • suricata增加单元测试编译失败
  • cursor 出现 unauthorized request