【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
函数的输出中我们可以清楚地看到:
append
函数不会修改原切片,而是返回一个新的切片。如果忘记将append
的结果重新赋值给原切片变量,那么原切片将不会发生任何改变。- 当
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)
的 len
和 cap
都设置为 cap
,这意味着我们预先分配了一个指定容量的底层数组。我们自定义 Array
的 len
字段(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
:根据索引获取元素,包含越界检查。Len
和Cap
:分别返回当前数组的实际长度和容量。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
结构体成功实现了动态扩容的功能,并且 len
和 cap
的变化也符合预期。
四、总结
通过本文的讲解和示例,我们不仅回顾了 Go 语言内置 slice
的核心特性,包括其 len
和 cap
的概念以及 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))
}