Go 复合类型:数组和切片
数组 (Arrays) 和切片 (Slices) 是 Go 语言中最基础的复合数据类型,它们允许你将多个相同类型的值组合在一个变量名下。
简单来说,数组提供了一块固定大小、连续的内存空间来存储元素;而切片则提供了一种更灵活、动态的视角,用于访问和操作底层的数组。
本章将深入探讨数组和切片的底层逻辑,涵盖它们的声明、初始化、操作技巧,以及在 Go 语言中特有的注意事项。
1. Go 语言中的数组 (Arrays)
数组是具有固定长度的相同类型元素的序列。在 Go 语言中,数组的长度是其类型的一部分。这意味着 [3]int 和 [4]int 是两种完全不同的类型,不能直接混用。
1.1 声明数组
声明数组的基本语法如下:
var 数组名 [数组长度]数据类型例如,声明一个可以容纳 5 个整数的数组:
var numbers [5]int这行代码声明了一个名为 numbers 的数组。它的元素会自动被初始化为该类型的零值(对于整数,零值就是 0)。
1.2 初始化数组
你可以通过多种方式在声明时直接初始化数组:
1. 使用字面量直接赋值:
numbers := [5]int{1, 2, 3, 4, 5}这会将 numbers 数组的元素依次初始化为 1 到 5。
2. 仅指定特定索引位置的元素:
numbers := [5]int{0: 10, 4: 50}这会将第一个元素(索引 0)设为 10,最后一个元素(索引 4)设为 50。未指定的剩余元素将保持零值 (0)。
3. 使用 ... 让编译器自动推断长度:
numbers := [...]int{1, 2, 3, 4, 5}这里的 ... 告诉编译器:“请根据大括号里元素的个数,自动帮我计算数组的长度。”
1.3 访问与修改数组元素
你可以通过索引 (Index) 来访问数组元素,索引从 0 开始计算:
numbers := [5]int{10, 20, 30, 40, 50}
fmt.Println(numbers[0]) // 输出: 10
fmt.Println(numbers[3]) // 输出: 40同样,你可以通过索引来修改对应位置的值:
numbers[1] = 25
fmt.Println(numbers[1]) // 输出: 251.4 数组的长度
数组的长度是固定的,你可以使用内置的 len() 函数来获取它:
numbers := [5]int{1, 2, 3, 4, 5}
length := len(numbers)
fmt.Println(length) // 输出: 5注意:在 Go 语言中,数组没有独立于长度的“容量”概念。
1.5 核心重点:数组是“值类型” (Values)
这是 Go 语言数组的一个极其关键的特性:数组是值类型。当你把一个数组赋值给另一个数组变量,或者把它作为参数传递给函数时,Go 会完整地复制整个数组的内容。对副本的任何修改,都绝对不会影响到原始数组。
numbers1 := [3]int{1, 2, 3}
numbers2 := numbers1 // numbers2 获得了 numbers1 的完整拷贝
numbers2[0] = 100
fmt.Println(numbers1) // 输出: [1 2 3] (原数组毫发无损)
fmt.Println(numbers2) // 输出: [100 2 3]这与许多其他编程语言(如 Java 或 Python,它们通常传递的是引用/指针)的逻辑完全不同,千万要牢记!
2. Go 语言中的切片 (Slices)
由于数组的长度固定且传递时会复制全量数据,这在实际开发中往往不够灵活。因此,Go 引入了切片 (Slices)。切片提供了一种更强大、更灵活的方式来处理数据序列。
切片本质上是对底层数组一个连续片段的“描述符” (Descriptor)。它内部包含了三个关键信息:
- 指向底层数组的指针。
- 切片的长度 (Length)。
- 切片的容量 (Capacity)。
2.1 声明切片
声明切片的语法与数组类似,但不需要指定长度:
var sliceName []dataType例如,声明一个整数切片:
var numbers []int2.2 创建切片
你可以通过以下几种主要方式创建切片:
1. 从现有数组中切取 (Slicing):
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:4] // 从索引 1 (包含) 切割到索引 4 (不包含)
fmt.Println(slice) // 输出: [2 3 4]这创建了一个切片,它“看向”原始 array 的一部分。
2. 使用 make() 函数动态创建:
slice := make([]int, 5) // 创建一个长度为 5,容量为 5 的切片
slice2 := make([]int, 5, 10) // 创建一个长度为 5,容量为 10 的切片make() 函数会自动在内存里分配一个隐藏的底层数组,并返回一个指向它的切片。第一个参数是切片类型,第二个是长度,第三个(可选)是容量。如果不传第三个参数,容量默认等于长度。
3. 使用切片字面量 (最常用):
slice := []int{1, 2, 3, 4, 5}这种写法看起来像数组字面量,但括号里没有数字或 ...。Go 会自动为你创建一个底层数组,并返回一个包含这些值的切片。
2.3 长度 (Length) 与容量 (Capacity)
- 长度 (Length): 切片当前实际包含的元素个数。
- 容量 (Capacity): 从切片的第一个元素开始,到底层数组末尾的总元素个数。
你可以分别使用 len() 和 cap() 函数来获取它们:
slice := make([]int, 3, 5)
fmt.Println(len(slice)) // 输出: 3
fmt.Println(cap(slice)) // 输出: 52.4 动态追加元素 (Append)
切片是动态的,你可以随时向其中添加新元素。Go 提供了内置的 append() 函数来实现这一点:
slice := []int{1, 2, 3}
slice = append(slice, 4, 5)
fmt.Println(slice) // 输出: [1 2 3 4 5]扩容机制: 如果切片当前的容量 (Capacity) 不足以容纳新追加的元素,Go 会自动在内存中申请一块更大的新数组,把老数据拷贝过去,然后追加新元素,最后让切片指向这个全新的底层数组。
2.5 对切片进行再切割 (Slicing Slices)
你可以基于一个现有的切片,再切出一个新的切片:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:4] // 从索引 1 到 4
fmt.Println(slice2) // 输出: [2 3 4]极其危险的陷阱: 因为多个切片可能共享同一个底层数组,修改其中一个切片的元素,往往会影响到其他切片!
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[1:4]
slice2[0] = 100 // 修改了 slice2 的第一个元素
fmt.Println(slice1) // 输出: [1 100 3 4 5] (slice1 也被改变了!)
fmt.Println(slice2) // 输出: [100 3 4]但请注意,如果对其中一个切片使用 append() 触发了扩容操作(生成了新的底层数组),它们就会“分道扬镳”,后续的修改就不再互相影响了。
2.6 独立拷贝切片 (Copying Slices)
如果你想要创建一个与原切片完全独立、互不干扰的副本,必须使用内置的 copy() 函数:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice1)) // 必须先分配足够长度的内存
copy(slice2, slice1) // 把 slice1 的内容拷贝到 slice2 中
slice2[0] = 100 // 修改副本
fmt.Println(slice1) // 输出: [1 2 3 4 5] (原切片不受影响)
fmt.Println(slice2) // 输出: [100 2 3 4 5]copy() 函数拷贝的元素数量,取决于两个切片中较短的那个的长度。
2.7 切片的零值 (Zero Value)
切片的零值是 nil。一个 nil 切片的长度和容量都是 0,并且它没有指向任何底层数组。
var slice []int
fmt.Println(slice == nil) // 输出: true
fmt.Println(len(slice)) // 输出: 0
fmt.Println(cap(slice)) // 输出: 0不过不用担心,Go 非常智能。你可以直接对一个 nil 切片使用 append(),Go 会自动为你分配底层数组。
3. 如何选择:数组 vs 切片?
| 特性 | 数组 (Array) | 切片 (Slice) |
|---|---|---|
| 长度 | 固定不变 (编译时确定) | 动态可变 (运行时扩缩容) |
| 内存表现 | 值类型 (赋值时拷贝全量数据) | 引用类型 (底层是指针结构) |
| 适用场景 | 明确知道数据大小且绝不会变;对内存分配有极致性能要求的底层场景 | 绝大多数日常开发场景;需要动态增删数据的列表 |
结论: 在 Go 语言中,99% 的情况下你都应该使用切片。
4. 实战代码示例
来看看切片在实际编程中的威力。
示例 1:计算一组数字的平均值
package main
import "fmt"
func main() {
numbers := []float64{10.5, 20.7, 30.2, 40.9, 50.1}
sum := 0.0
// 使用 range 遍历切片,'_' 表示忽略索引
for _, number := range numbers {
sum += number
}
average := sum / float64(len(numbers))
fmt.Printf("平均值: %.2f\n", average) // 输出: 平均值: 30.50
}示例 2:过滤出所有的偶数
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
evenNumbers := []int{} // 初始化一个空切片
for _, number := range numbers {
if number%2 == 0 {
// 利用 append 动态添加元素
evenNumbers = append(evenNumbers, number)
}
}
fmt.Println("所有的偶数:", evenNumbers) // 输出: 所有的偶数: [2 4 6 8 10]
}示例 3:利用切片实现“栈” (Stack) 数据结构
package main
import "fmt"
type Stack struct {
data []int
}
// Push: 入栈,将元素追加到切片末尾
func (s *Stack) Push(value int) {
s.data = append(s.data, value)
}
// Pop: 出栈,弹出切片末尾的元素
func (s *Stack) Pop() (int, bool) {
if len(s.data) == 0 {
return 0, false // 栈为空
}
index := len(s.data) - 1
value := s.data[index]
s.data = s.data[:index] // 切割切片,移除最后一个元素
return value, true
}
func main() {
stack := Stack{data: []int{}}
stack.Push(10)
stack.Push(20)
stack.Push(30)
value, ok := stack.Pop()
if ok {
fmt.Println("出栈元素:", value) // 输出: 出栈元素: 30
}
value, ok = stack.Pop()
if ok {
fmt.Println("出栈元素:", value) // 输出: 出栈元素: 20
}
}