【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 的结果赋值回 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 函数的输出中我们可以清楚地看到:

  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 = array
    s.cap = cap
    s.len = 0 // 实际有效元素数量初始化为 0
    return 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 变为 5
    fmt.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 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))
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值