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 的结果赋值回 array
array = 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 变为 4
fmt.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 = array
s.cap = cap
s.len = 0 // 实际有效元素数量初始化为 0
return 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 变为 5
fmt.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 main
import (
"fmt"
"sync"
)
// 切片的使用
func main1() {
// cap :2
array := 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 []int
len int
cap int
lock 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 = array
s.cap = cap
s.len = 0
return s
}
// 添加元素
func (a *Array) Append(element int) {
a.lock.Lock()
defer a.lock.Unlock()
if a.len == a.cap {
// 扩容
newCap := 2 * a.len
// 如果之前容量为0,那么新容量为1
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
}
// 增加多个元素
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))
}