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

【Go】P8 Go 语言核心数据结构:深入解析切片 (Slice)

目录

  • 前言
  • 什么是切片?
    • 切片的声明与初始化
    • 基于数组定义切片
    • 切片再切片
    • 使用 make() 函数构造切片
  • 切片的操作
    • 使用 append() 函数追加元素
    • 切片的扩容策略
    • 切片的循环遍历
    • 切片中的拷贝 (copy 函数)
    • 从切片中删除元素
  • 数组切片的排序算法
    • 选择排序 (Selection Sort)
    • 冒泡排序 (Bubble Sort)
    • sort 包 (Go 语言标准库)
  • 总结

在这里插入图片描述

前言

大家好! 在上一篇博文中,我们详细分享了数组(Array)的内容。我们知道,数组是一个拥有相同类型元素固定长度序列。但在实际开发中,我们更常需要处理可变长度的序列。今天,我们就来深入探讨 Go 语言中一个极其重要且强大的数据结构——切片(Slice)

切片是 Go 语言的灵魂之一,它提供了比数组更强大、更灵活的序列操作能力。理解切片,是成为一名合格 Go 程序员的必经之路。


什么是切片?

简单来说,切片(Slice)是一个拥有相同类型元素可变长度序列。

它听起来和数组很像,但关键区别在于“可变长度”。切片是对数组的一层封装,它本身并不存储任何数据,而是“引用”一个底层的数组。

切片是一个引用类型,它的内部结构包含了三个核心字段:

  • 指针 (ptr): 指向底层数组中该切片引用的第一个元素。
  • 长度 (len): 切片中实际包含的元素个数。
  • 容量 (cap): 从切片的第一个元素开始,到底层数组末尾的元素个数。

切片的声明与初始化

声明切片的基本语法如下:

var name []T

其中 T 是切片中元素的类型。

注意: 仅仅声明一个切片时,它的默认值为 nil。一个 nil 切片的长度和容量都是 0,并且没有指向任何底层数组。

package mainimport "fmt"func main() {// 1. 声明一个 nil 切片var s1 []intfmt.Printf("s1: %v, len: %d, cap: %d, is nil: %t\n", s1, len(s1), cap(s1), s1 == nil)// 输出: s1: [], len: 0, cap: 0, is nil: true// 2. 使用字面量初始化 (最常用)s2 := []int{1, 2, 3, 4}fmt.Printf("s2: %v, len: %d, cap: %d, is nil: %t\n", s2, len(s2), cap(s2), s2 == nil)// 输出: s2: [1 2 3 4], len: 4, cap: 4, is nil: false// 3. 声明一个空切片 (与 nil 切片不同)s3 := []int{}fmt.Printf("s3: %v, len: %d, cap: %d, is nil: %t\n", s3, len(s3), cap(s3), s3 == nil)// 输出: s3: [], len: 0, cap: 0, is nil: false
}

nil 切片和空切片虽然在 lencap 上都为 0,但 nil 切片不指向任何底层内存,而空切片则指向了一个底层的空数组。

基于数组定义切片

切片的核心在于它是数组的“视图”。我们可以通过“切片表达式”从一个数组或另一个切片中创建新切片。

语法:a[low : high]

  • low:起始索引(包含)
  • high:结束索引(不包含)

这将创建一个新切片,其长度为 high - low

package mainimport "fmt"func main() {// 1. 先定义一个数组arr := [5]int{10, 20, 30, 40, 50}// 2. 基于数组创建切片// 取 arr[1] 到 arr[3] (不包括 arr[4])s1 := arr[1:4] fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))// 输出: s1: [20 30 40], len: 3, cap: 4// 为什么 cap 是 4?// 因为切片的容量是从它的第一个元素(arr[1])// 一直到底层数组的最后一个元素(arr[4])。// 个数是 20(arr[1]), 30(arr[2]), 40(arr[3]), 50(arr[4]),总共 4 个。
}

重要: 切片是引用类型。修改切片中的元素,会直接修改底层数组中对应的值。

package mainimport "fmt"func main() {arr := [5]int{10, 20, 30, 40, 50}s1 := arr[1:4] // s1 = [20, 30, 40]fmt.Println("修改前: arr =", arr) // arr = [10 20 30 40 50]// 修改切片 s1 的第一个元素 (对应 arr[1])s1[0] = 200fmt.Println("修改后: arr =", arr) // arr = [10 200 30 40 50]fmt.Println("修改后: s1 =", s1) // s1 = [200 30 40]
}

切片再切片

我们也可以在一个已有的切片上再次进行切片操作,创建子切片。

package mainimport "fmt"func main() {s1 := []int{1, 2, 3, 4, 5, 6}// s2 是 s1 的子切片s2 := s1[1:3] // [2, 3]fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))// 输出: s2: [2 3], len: 2, cap: 5  (容量从 s1[1] 到 s1[5] 共 5 个)// s3 是 s2 的子切片s3 := s2[1:] // [3]  (从 s2[1] 到末尾)fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))// 输出: s3: [3], len: 1, cap: 4 (容量从 s2[1] -> s1[2] 到 s1[5] 共 4 个)// 它们都引用同一个底层数组s3[0] = 300fmt.Println("s1:", s1) // s1: [1 2 300 4 5 6]fmt.Println("s2:", s2) // s2: [2 300]fmt.Println("s3:", s3) // s3: [300]
}

切片表达式的完整语法: a[low : high : max]

  • low:起始索引(包含)
  • high:结束索引(不包含)
  • max:容量上限(不包含,用于限制新切片的 cap)

新切片的 len = high - lowcap = max - low。这是一种高级用法,可以用来防止子切片通过 append 操作意外地覆盖父切片的数据。

使用 make() 函数构造切片

如果我们不想依赖一个已存在的数组,而是想直接创建一个动态的切片,make() 函数是最好的选择。

make() 函数会分配一个底层的数组,并返回一个指向它的切片。

// 格式1: make([]T, len)
// 创建一个类型为 T,长度和容量都为 len 的切片
s1 := make([]int, 5) 
// s1: [0 0 0 0 0], len: 5, cap: 5// 格式2: make([]T, len, cap)
// 创建一个类型为 T,长度为 len,容量为 cap 的切片
s2 := make([]int, 3, 10)
// s2: [0 0 0], len: 3, cap: 10

切片的操作

使用 append() 函数追加元素

append() 是切片最常用的函数,用于向切片末尾追加一个或多个元素。

package mainimport "fmt"func main() {var s []int // s 是一个 nil 切片// 1. 追加单个元素s = append(s, 1) // s = [1]// 2. 追加多个元素s = append(s, 2, 3, 4) // s = [1, 2, 3, 4]// 3. 追加另一个切片 (注意...语法)s2 := []int{5, 6}s = append(s, s2...) // s = [1, 2, 3, 4, 5, 6]fmt.Println(s)
}

重点: append() 的返回值
append 函数必须将结果重新赋值给原切片,即 s = append(s, ...)。 为什么?这就涉及到了切片的扩容策略

切片的扩容策略

当我们使用 append 时,如果切片的 len 小于 cap,元素会直接添加到底层数组中,len 增加,切片头结构 (ptr, len, cap) 中的 len 被更新。

但是,当 len 等于 cap 时,切片已满,无法再容纳新元素。此时,Go 的运行时会:

  • 分配一个新数组: 这个新数组的容量会比旧数组大。
  • 复制数据: 将旧数组中的所有元素复制到新数组中。
  • 添加新元素: 在新数组末尾添加要 append 的元素。
  • 返回新切片: append 函数返回一个指向这个新数组的新切片。

这就是为什么我们必须 s = append(s, ...),因为 s 可能已经指向了一个全新的底层数组。

仅需了解:
扩容的规则(大致策略,不同 Go 版本可能微调)

  • 如果切片容量小于 1024(在某些版本是 256),新容量会翻倍(cap * 2)。
  • 如果切片容量大于等于 1024,新容量会增长 1.25 倍(cap * 1.25),以避免内存的过度浪费。
package mainimport "fmt"func main() {s := make([]int, 0, 1) // len=0, cap=1fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)s = append(s, 1) // len=1, cap=1fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)// 此时 cap=1, len=1,再 append 会触发扩容 (cap 翻倍到 2)s = append(s, 2) // len=2, cap=2fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s) // 注意看 ptr 地址变化// 此时 cap=2, len=2,再 append 会触发扩容 (cap 翻倍到 4)s = append(s, 3) // len=3, cap=4fmt.Printf("len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s) // ptr 再次变化
}

(注意:%p 打印出的地址在每次运行时都可能不同)

切片的循环遍历

和数组一样,切片主要有两种遍历方式:

package mainimport "fmt"func main() {s := []string{"Apple", "Banana", "Cherry"}// 方式一: for 循环 (传统方式)fmt.Println("--- for 循环 ---")for i := 0; i < len(s); i++ {fmt.Printf("Index: %d, Value: %s\n", i, s[i])}// 方式二: for range 循环 (Go 推荐)fmt.Println("--- for range 循环 ---")for index, value := range s {fmt.Printf("Index: %d, Value: %s\n", index, value)}// 如果你只关心值 (value)fmt.Println("--- for range (仅 value) ---")for _, value := range s {fmt.Println("Value:", value)}
}

切片中的拷贝 (copy 函数)

因为切片是引用类型,我们不能简单地用 s2 := s1 来创建一个独立的切片。

  • s2 := s1 这只是浅拷贝(Shallow Copy)s1s2 共享同一个底层数组。修改 s2 会影响 s1

要想创建两个完全独立的切片(互不影响),我们需要使用内置的 copy() 函数进行深拷贝(Deep Copy)

  • copy(dst, src)src 切片中的元素复制到 dst 切片中。
package mainimport "fmt"func main() {s1 := []int{1, 2, 3}// 陷阱:浅拷贝s2 := s1s2[0] = 100fmt.Println("s1 (浅拷贝后):", s1) // s1: [100 2 3] (s1 被改变了!)// 正确:深拷贝s3 := []int{1, 2, 3}// 1. 创建一个和 s3 相同长度的目标切片s4 := make([]int, len(s3))// 2. 拷贝元素copy(s4, s3)// 3. 修改 s4s4[0] = 999fmt.Println("s3 (深拷贝后):", s3) // s3: [1 2 3] (s3 未受影响)fmt.Println("s4 (深拷贝后):", s4) // s4: [999 2 3]
}

从切片中删除元素

Go 语言中没有提供直接的 “delete” 语法来删除切片中的元素。但我们可以利用 append 的特性来实现删除。

其原理是:将 “被删除元素” 之后的所有元素,拼接到 “被删除元素” 之前的所有元素上

// 假设要从 s 中删除索引为 index 的元素
s = append(s[:index], s[index+1:]...)
  • s[:index] 获取 0index-1 的所有元素。
  • s[index+1:]... 获取 index+1 到末尾的所有元素(并使用 ... 展开)。
package mainimport "fmt"func main() {s := []string{"A", "B", "C", "D", "E"}// 假设我们要删除索引 2 的元素 ("C")indexToRemove := 2s = append(s[:indexToRemove], s[indexToRemove+1:]...)fmt.Println("删除后:", s) // 输出: [A B D E]
}

警告: 这种删除方式会修改底层数组(被删除元素后面的元素会前移)。


数组切片的排序算法

最后,我们来看看如何对切片进行排序。我们介绍三种方法:两种经典的排序算法实现,以及 Go 的标准库 sort

选择排序 (Selection Sort)

逻辑:

  1. 找到切片中最小的元素。
  2. 将它与切片的第一个元素交换位置。
  3. 在剩下的元素中,找到最小的元素。
  4. 将它与切片的第二个元素交换位置。
  5. …以此类推,直到整个切片排序完成。
package mainimport "fmt"func selectionSort(items []int) {n := len(items)for i := 0; i < n; i++ {// 假定当前 i 是最小值的索引minIndex := i// 遍历 i 后面的元素,找到真正的最小值索引for j := i + 1; j < n; j++ {if items[j] < items[minIndex] {minIndex = j}}// 将找到的最小值与 i 位置交换items[i], items[minIndex] = items[minIndex], items[i]}
}func main() {s := []int{5, 2, 8, 1, 9, 4}selectionSort(s)fmt.Println("选择排序后:", s) // [1 2 4 5 8 9]
}

冒泡排序 (Bubble Sort)

逻辑:

  1. 比较相邻的两个元素。
  2. 如果第一个比第二个大,就交换它们。
  3. 对切片中的每一对相邻元素做同样的工作,从开始到结束。这会使最大的元素“冒泡”到最后。
  4. 重复以上步骤,但每次比较的范围缩小 1(因为最大的已经归位了)。
  5. …直到没有元素需要交换。
package mainimport "fmt"func bubbleSort(items []int) {n := len(items)for i := 0; i < n; i++ {// 标记这一轮是否发生了交换swapped := false// (n-1-i) 是因为每轮都会把最大的放到最后,所以比较范围缩小for j := 0; j < n-1-i; j++ {if items[j] > items[j+1] {// 交换items[j], items[j+1] = items[j+1], items[j]swapped = true}}// 如果这一轮没有发生交换,说明已经排好序了if !swapped {break}}
}func main() {s := []int{5, 2, 8, 1, 9, 4}bubbleSort(s)fmt.Println("冒泡排序后:", s) // [1 2 4 5 8 9]
}

sort 包 (Go 语言标准库)

虽然理解冒泡和选择排序很重要,但在实际工作中,我们永远应该使用 Go 标准库 sort 包。它实现了更高效的排序算法(如快速排序)。

sort 包为 []int, []string, []float64 提供了开箱即用的排序方法。

package mainimport ("fmt""sort"
)func main() {// 1. 排序 []intsInts := []int{5, 2, 8, 1, 9, 4}sort.Ints(sInts)fmt.Println("sort.Ints:", sInts) // [1 2 4 5 8 9]// 2. 排序 []stringsStrings := []string{"Banana", "Apple", "Cherry"}sort.Strings(sStrings)fmt.Println("sort.Strings:", sStrings) // [Apple Banana Cherry]// 3. 自定义排序 (例如,按长度排序字符串)sort.Slice(sStrings, func(i, j int) bool {// 定义 "小于" 规则:// 如果 sStrings[i] 的长度小于 sStrings[j] 的长度,// 那么 sStrings[i] 应该排在前面。return len(sStrings[i]) < len(sStrings[j])})fmt.Println("sort.Slice (by len):", sStrings) // [Apple Cherry Banana]
}

sort.Slice 极其强大,它允许你根据任何自定义逻辑对任意类型的切片进行排序,读者请在实际项目中尝试并理解。


总结

切片是 Go 语言中最灵活、最常用的数据结构。掌握它,你的 Go 编程能力将更上一层楼。

关键点回顾:

  • 切片是引用类型: 它包含 指针、长度 (len) 和 容量 (cap)。
  • 基于数组创建: arr[low:high] 创建的切片与原数组共享内存
  • make() 创建: make([]T, len, cap) 是创建动态切片的标准方式。
  • append() 扩容:len == cap 时,append 会分配新数组,必须 s = append(s, ...)
  • copy() 拷贝: copy(dst, src) 是实现 深拷贝(独立切片) 的唯一途径。
  • sort 包排序: 始终优先使用标准库 sort.Intssort.Slice 进行排序。

希望这篇博文能帮助你彻底理解 Go 语言的切片!在下一篇博文中,我们将探讨 Go 的另一个核心数据结构:map。

感谢阅读!


2025.10.20 金融街

http://www.dtcms.com/a/507805.html

相关文章:

  • 使用Wireshark测试手机APP网络通信完整指南
  • 【AI论文】MemMamba:对状态空间模型中记忆模式的重新思考
  • 郴州建站扁平化网站后台
  • 请问做网站和编程哪个容易些网站建设一般的流程
  • 三地两中心架构介绍
  • Harmony鸿蒙开发0基础入门到精通Day01--JavaScript篇
  • CCIE好像越来越销声匿迹了......
  • 自己做ppt网站汕头网站制作哪里好
  • UVa 1344 Tian Ji The Horse Racing
  • 网站交换链接友情链接的作用网站地图制作
  • 【给服务器安装服务器安装nacos】
  • 影楼模板网站html5风格网站特色
  • Spark的Shuffle过程
  • 前端HTML常用基础标
  • 智能井盖传感器如何成为智慧城市“无声卫士”?
  • Django Web 开发系列(一):视图基础与 URL 路由配置全解析
  • 【python】在Django中,执行原生SQL查询
  • 5 个 Windows 故障排除工具
  • 云南网站建设招商交换友情链接的渠道
  • 在SCNet使用异构海光DCU 部署文心21B大模型报错HIP out of memory(未调通)
  • 嘉兴网站建设优化温州快速建站公司
  • 西安自助建站公司网站没有做404页面
  • 解决Vcenter告警datastore存储容量不足问题
  • 骆驼重链抗体免疫文库构建:从动物免疫到文库质控的关键技术解析
  • BearPi小熊派 鸿蒙开发入门笔记(1)
  • 湖州品牌网站设计wordpress侧栏导航栏
  • 使用EasyExcel生成下拉列表
  • 解密面向对象三大特征:封装、继承、多态
  • 未来之窗昭和仙君(二十六)复制指定元素内容到剪贴板——东方仙盟筑基期
  • nginx压缩包在windows下如何启动和停止使用nginx