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

【Go-补充】实现动态数组:深入理解 slice 与自定义实现


Go 语言实现动态数组:深入理解 slice 与自定义实现

在 Go 语言中,我们经常使用内置的 slice (切片) 来处理可变长度的序列数据。slice 是 Go 语言中一个强大且灵活的数据结构,它在底层是对数组的抽象,提供了动态增长的能力。本文将通过一个实际的例子,带你深入理解 slice 的工作原理,并展示如何基于 slice 自行实现一个具有动态扩容能力的数组结构。


一、理解 Go 语言内置 slice 的核心机制

Go 语言的 slice 并非一个简单的数组,它是一个包含三个字段的结构体:指向底层数组的指针、长度 (len) 和容量 (cap)。

  • 长度 (len)slice 中当前实际存储的元素数量。
  • 容量 (cap)slice 底层数组从 slice 的起始位置开始,到其底层数组的结束位置的元素数量。

让我们通过 main1 函数的例子来观察 slice 的行为:

func main1() {// 初始创建一个 len 为 0, cap 为 2 的切片array := make([]int, 0, 2)fmt.Println("cap", cap(array), "len", len(array), "array", array) // cap 2 len 0 array []// 第一次 append,但没有将结果赋值回 array// append 函数会返回一个新的切片,如果原切片容量不足会进行扩容,并返回扩容后的新切片// 但这里没有赋值,所以 array 仍然是原来的 array_ = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 2 len 0 array: []_ = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 2 len 0 array: []_ = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 2 len 0 array: []fmt.Println("-------")// 将 append 的结果赋值回 arrayarray = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 2 len 1 array: [1]array = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 2 len 2 array: [1 1]// 此时 len == cap,再次 append 会触发扩容array = append(array, 1)// 扩容后,通常容量会翻倍(或根据一定策略增长),这里从 2 变为 4fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 4 len 3 array: [1 1 1]array = append(array, 1, 1, 1, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 8 len 7 array: [1 1 1 1 1 1 1]array = append(array, 1, 1, 1, 1, 1, 1, 1, 1, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array) // cap 16 len 16 array: [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
}

在这里插入图片描述

main1 函数的输出中我们可以清楚地看到:

  1. append 函数不会修改原切片,而是返回一个新的切片。如果忘记将 append 的结果重新赋值给原切片变量,那么原切片将不会发生任何改变。
  2. slice 的长度达到其容量时,再次 append 会触发扩容。Go 运行时会分配一个更大的底层数组,将原有数据复制过去,然后返回一个指向新底层数组的新 slice。扩容策略通常是容量翻倍,但具体策略可能因 Go 版本和实际情况而异。

二、自定义实现一个可变长数组

虽然 Go 语言的 slice 已经足够强大,但在某些场景下,例如为了更好地理解 slice 的工作机制,或者需要添加一些特定的并发控制或业务逻辑时,我们可能需要自行实现一个类似可变长数组的数据结构。

这里我们实现了一个 Array 结构体,它包含以下字段:

  • array []int:底层的 slice,用于存储实际数据。
  • len int:当前数组中实际存储的元素数量。
  • cap int:底层 slice 的容量。
  • lock sync.Mutex:用于并发安全的互斥锁,确保在多 goroutine 环境下对数组的操作是线程安全的。
1. Make 函数:初始化数组

Make 函数用于初始化一个 Array 实例。

// 初始化数组
func Make(len, cap int) *Array {s := new(Array)if len > cap {panic("len large than cap") // 长度不能大于容量}// 把切片当数组用,初始时 len 和 cap 都设置为 cap// 这样做的目的是为了预先分配好底层数组的空间array := make([]int, cap, cap)s.array = arrays.cap = caps.len = 0 // 实际有效元素数量初始化为 0return s
}

注意:这里 make([]int, cap, cap)lencap 都设置为 cap,这意味着我们预先分配了一个指定容量的底层数组。我们自定义 Arraylen 字段(s.len)则初始化为 0,表示当前没有有效数据。

2. Append 方法:添加单个元素

Append 方法负责向数组中添加一个元素,并处理扩容逻辑。

// 添加元素
func (a *Array) Append(element int) {a.lock.Lock() // 加锁,保证并发安全defer a.lock.Unlock() // 函数结束时解锁if a.len == a.cap {// 扩容newCap := 2 * a.len // 新容量通常是旧容量的两倍// 如果之前容量为0,那么新容量为1(避免 0 * 2 还是 0)if a.cap == 0 {newCap = 1}newArray := make([]int, newCap, newCap) // 创建新的底层数组// 把老数组的数据移动到新数组for k, v := range a.array {newArray[k] = v}a.array = newArray // 更新底层数组a.cap = newCap     // 更新容量}// 把元素放在数组里a.array[a.len] = element // 将新元素放到当前有效长度的下一个位置a.len = a.len + 1        // 实际有效长度加 1
}

Append 方法的核心在于扩容机制。当 a.len == a.cap 时,表示当前底层数组已满,需要创建一个更大的新数组。这里我们采取了经典的容量翻倍策略。此外,为了防止从容量为 0 的数组开始扩容时依然是 0,我们做了特殊处理,将其扩容到 1。

3. 其他辅助方法
  • AppendMany:一次性添加多个元素,通过循环调用 Append 实现。
  • Get:根据索引获取元素,包含越界检查。
  • LenCap:分别返回当前数组的实际长度和容量。
  • Print:辅助打印数组内容,方便调试。
// 增加多个元素
func (a *Array) AppendMany(element ...int) {for _, v := range element {a.Append(v)}
}// 获取指定下标元素
func (a *Array) Get(index int) int {// 越界检查if a.len == 0 || index >= a.len {panic("index over len")}return a.array[index]
}// 获取真实长度和容量
func (a *Array) Len() int {return a.len
}
func (a *Array) Cap() int {return a.cap
}// 辅助打印
func Print(array *Array) (result string) {result = "["for i := 0; i < array.Len(); i++ {if i == 0 {result = fmt.Sprintf("%s%d", result, array.Get(i))continue}result = fmt.Sprintf("%s %d", result, array.Get(i))}result = result + "]"return
}

三、运行示例

最后,让我们在 main 函数中测试我们自定义的 Array

func main() {// 创建一个容量为3的动态数组a := Make(0, 3)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // cap 3 len 0 array: []// 增加一个元素a.Append(10)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // cap 3 len 1 array: [10]// 增加一个元素a.Append(9)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // cap 3 len 2 array: [10 9]// 增加多个元素 (此时 len 达到 3,下次 Append 将触发扩容)a.AppendMany(8, 7)// 第一次 Append(8): len=3, cap=3,触发扩容,cap 变为 6,len 变为 4// 第二次 Append(7): len=4, cap=6,不触发扩容,len 变为 5fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a)) // cap 6 len 4 array: [10 9 8 7]
}

在这里插入图片描述

从输出可以看出,我们自定义的 Array 结构体成功实现了动态扩容的功能,并且 lencap 的变化也符合预期。


四、总结

通过本文的讲解和示例,我们不仅回顾了 Go 语言内置 slice 的核心特性,包括其 lencap 的概念以及 append 时的扩容机制,还亲手实现了一个具有类似功能的动态数组。这个自定义实现加深了我们对 Go 语言底层数据结构和内存管理的理解,也展示了如何在 Go 中构建并发安全的可变长数据结构。希望这篇文章能帮助你更好地掌握 Go 语言中 slice 的精髓!

五、代码

package mainimport ("fmt""sync"
)// 切片的使用
func main1() {// cap :2array := make([]int, 0, 2)fmt.Println("cap", cap(array), "len", len(array), "array", array)// 虽然 append 但是没有赋予原来的变量 array_ = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)_ = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)_ = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)fmt.Println("-------")// 赋予回原来的变量array = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)array = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)array = append(array, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)array = append(array, 1, 1, 1, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)array = append(array, 1, 1, 1, 1, 1, 1, 1, 1, 1)fmt.Println("cap", cap(array), "len", len(array), "array:", array)
}// 实现可变长数组
type Array struct {array []intlen   intcap   intlock  sync.Mutex
}// 初始化数组
func Make(len, cap int) *Array {s := new(Array)if len > cap {panic("len large than cap")}// 把切片当数组用array := make([]int, cap, cap)s.array = arrays.cap = caps.len = 0return s
}// 添加元素
func (a *Array) Append(element int) {a.lock.Lock()defer a.lock.Unlock()if a.len == a.cap {// 扩容newCap := 2 * a.len// 如果之前容量为0,那么新容量为1if a.cap == 0 {newCap = 1}newArray := make([]int, newCap, newCap)// 把老数组的数据移动到新数组for k, v := range a.array {newArray[k] = v}a.array = newArraya.cap = newCap}// 把元素放在数组里a.array[a.len] = elementa.len = a.len + 1
}// 增加多个元素
func (a *Array) AppendMany(element ...int) {for _, v := range element {a.Append(v)}
}// 获取指定下标元素
func (a *Array) Get(index int) int {// 越界if a.len == 0 || index >= a.len {panic("index over len")}return a.array[index]
}// 获取真实长度和容量
func (a *Array) Len() int {return a.len
}
func (a *Array) Cap() int {return a.cap
}// 辅助打印
func Print(array *Array) (result string) {result = "["for i := 0; i < array.Len(); i++ {// 第一个元素if i == 0 {result = fmt.Sprintf("%s%d", result, array.Get(i))continue}result = fmt.Sprintf("%s %d", result, array.Get(i))}result = result + "]"return
}func main() {// 创建一个容量为3的动态数组a := Make(0, 3)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))// 增加一个元素a.Append(10)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))// 增加一个元素a.Append(9)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))// 增加多个元素a.AppendMany(8, 7)fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))
}
http://www.xdnf.cn/news/995293.html

相关文章:

  • 机器学习 [白板推导](六)[核方法、指数族分布]
  • 【WebSocket】WebSocket架构重构:从分散管理到统一连接的实战经验
  • 新零售视域下实体与虚拟店融合的技术逻辑与商业模式创新——基于开源AI智能名片与链动2+1模式的S2B2C生态构建
  • C#事件基础模型代码
  • 【技术追踪】MMFusion:用于食管癌淋巴结转移诊断的多模态扩散模型(MICCAI-2024)
  • Linux部署bmc TrueSight 监控agent步骤
  • Java学习笔记之:初识nginx
  • js判断手机操作系统(ios、安卓、华为)
  • 分享在日常开发中常用的ES6知识点【面试常考】
  • “储能+热泵+AI”三维驱动,美的能源定义能源科技新未来
  • 【深度解读】混合架构数据保护实战
  • 从零搭建智能家居:香橙派+HomeAssistant实战指南
  • LlamaIndex 工作流 上下文状态和流式传输事件
  • SpringBoot+Junit在IDEA中实现查询数据库的单元测试
  • 代码训练LeetCode(32)Z字形变换
  • chrome138版本及以上el-input的textarea输入问题
  • 鸿蒙北向应用开发:新增ts文件出现的问题
  • 【狂飙AGI】第1课:大模型概述
  • QT+VTK 中QWidget与QVTKOpenGLNativeWidget的使用
  • python打卡第52天
  • 如何从 Ansys SpaceClaim 模型中提取 CAD 数据,该模型是在我计算机上安装的未来版本中创建的?
  • Kafka问题排查笔记
  • 全局搜索正则表达式grep
  • 用volatile修饰数组代表什么意思,Java
  • physicsnemo开源程序是开源深度学习框架,用于使用最先进的 Physics-ML 方法构建、训练和微调深度学习模型
  • 接到数据分析任务后,怎么判断是分类还是回归?什么时候你该考虑换模型?
  • Centos8 安装 达梦数据库
  • OpenLayers 加载格网和经纬网
  • STM32通用定时器TRC含义解析
  • 【数据传输常用命令】:服务器与本地之间的数据传输