1.4.1 数组与切片 #
数组和切片是 Go 语言中用于存储序列数据的两种重要数据类型。虽然它们在功能上相似,但在内存管理、性能特点和使用方式上存在显著差异。理解这两种数据类型的特点和适用场景,是掌握 Go 语言编程的重要基础。
数组(Array) #
数组的基本概念 #
数组是具有固定长度的同类型元素序列。在 Go 语言中,数组的长度是类型的一部分,这意味着 [3]int
和 [4]int
是完全不同的类型。数组在编译时就确定了大小,存储在栈上,具有很好的性能特点。
数组的声明和初始化 #
package main
import "fmt"
func main() {
// 1. 声明数组的几种方式
// 方式一:声明后赋值
var numbers [5]int
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
fmt.Printf("数组 numbers: %v\n", numbers)
// 方式二:声明时初始化
var fruits = [3]string{"苹果", "香蕉", "橙子"}
fmt.Printf("水果数组: %v\n", fruits)
// 方式三:使用短变量声明
colors := [4]string{"红色", "绿色", "蓝色", "黄色"}
fmt.Printf("颜色数组: %v\n", colors)
// 方式四:让编译器推断长度
scores := [...]int{85, 92, 78, 96, 88}
fmt.Printf("成绩数组: %v, 长度: %d\n", scores, len(scores))
// 方式五:指定索引初始化
weekdays := [7]string{
0: "星期日",
1: "星期一",
2: "星期二",
6: "星期六", // 其他位置为零值
}
fmt.Printf("星期数组: %v\n", weekdays)
}
数组的基本操作 #
package main
import "fmt"
func main() {
// 创建一个整数数组
numbers := [5]int{10, 20, 30, 40, 50}
// 1. 访问数组元素
fmt.Printf("第一个元素: %d\n", numbers[0])
fmt.Printf("最后一个元素: %d\n", numbers[len(numbers)-1])
// 2. 修改数组元素
numbers[2] = 35
fmt.Printf("修改后的数组: %v\n", numbers)
// 3. 遍历数组
fmt.Println("使用传统 for 循环遍历:")
for i := 0; i < len(numbers); i++ {
fmt.Printf("索引 %d: 值 %d\n", i, numbers[i])
}
fmt.Println("使用 range 遍历:")
for index, value := range numbers {
fmt.Printf("索引 %d: 值 %d\n", index, value)
}
// 4. 只要值,不要索引
fmt.Println("只输出值:")
for _, value := range numbers {
fmt.Printf("%d ", value)
}
fmt.Println()
// 5. 数组比较
arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
arr3 := [3]int{1, 2, 4}
fmt.Printf("arr1 == arr2: %t\n", arr1 == arr2) // true
fmt.Printf("arr1 == arr3: %t\n", arr1 == arr3) // false
}
多维数组 #
package main
import "fmt"
func main() {
// 1. 二维数组声明和初始化
var matrix [3][4]int
// 填充二维数组
for i := 0; i < 3; i++ {
for j := 0; j < 4; j++ {
matrix[i][j] = i*4 + j + 1
}
}
fmt.Println("二维数组:")
for i := 0; i < 3; i++ {
for j := 0; j < 4; j++ {
fmt.Printf("%2d ", matrix[i][j])
}
fmt.Println()
}
// 2. 初始化时赋值
grid := [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
fmt.Println("网格数组:")
for _, row := range grid {
for _, value := range row {
fmt.Printf("%d ", value)
}
fmt.Println()
}
// 3. 三维数组示例
cube := [2][2][2]int{
{
{1, 2},
{3, 4},
},
{
{5, 6},
{7, 8},
},
}
fmt.Println("三维数组:")
for i, plane := range cube {
fmt.Printf("平面 %d:\n", i)
for j, row := range plane {
fmt.Printf(" 行 %d: ", j)
for _, value := range row {
fmt.Printf("%d ", value)
}
fmt.Println()
}
}
}
切片(Slice) #
切片的基本概念 #
切片是对数组的抽象,提供了动态数组的功能。切片本身不存储数据,而是引用底层数组的一个连续片段。切片由三个部分组成:指向底层数组的指针、长度(length)和容量(capacity)。
切片的创建方式 #
package main
import "fmt"
func main() {
// 1. 从数组创建切片
arr := [6]int{10, 20, 30, 40, 50, 60}
slice1 := arr[1:4] // 包含索引1到3的元素
fmt.Printf("从数组创建切片: %v\n", slice1)
// 2. 使用 make 函数创建切片
slice2 := make([]int, 5) // 长度为5,容量为5
slice3 := make([]int, 3, 10) // 长度为3,容量为10
fmt.Printf("make创建的切片2: %v, 长度: %d, 容量: %d\n",
slice2, len(slice2), cap(slice2))
fmt.Printf("make创建的切片3: %v, 长度: %d, 容量: %d\n",
slice3, len(slice3), cap(slice3))
// 3. 使用切片字面量
slice4 := []string{"Go", "Python", "Java", "JavaScript"}
fmt.Printf("字面量创建的切片: %v\n", slice4)
// 4. 从切片创建切片
slice5 := slice4[1:3]
fmt.Printf("从切片创建切片: %v\n", slice5)
// 5. 切片的切片操作详解
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("原始切片: %v\n", numbers)
// 各种切片操作
fmt.Printf("numbers[2:5]: %v\n", numbers[2:5]) // [2 3 4]
fmt.Printf("numbers[:4]: %v\n", numbers[:4]) // [0 1 2 3]
fmt.Printf("numbers[6:]: %v\n", numbers[6:]) // [6 7 8 9]
fmt.Printf("numbers[:]: %v\n", numbers[:]) // 完整切片
}
切片的动态操作 #
package main
import "fmt"
func main() {
// 1. append 操作
var slice []int
fmt.Printf("初始切片: %v, 长度: %d, 容量: %d\n",
slice, len(slice), cap(slice))
// 添加单个元素
slice = append(slice, 1)
slice = append(slice, 2, 3, 4)
fmt.Printf("添加元素后: %v, 长度: %d, 容量: %d\n",
slice, len(slice), cap(slice))
// 添加另一个切片
other := []int{5, 6, 7}
slice = append(slice, other...)
fmt.Printf("添加切片后: %v, 长度: %d, 容量: %d\n",
slice, len(slice), cap(slice))
// 2. copy 操作
source := []int{1, 2, 3, 4, 5}
dest := make([]int, len(source))
n := copy(dest, source)
fmt.Printf("复制了 %d 个元素\n", n)
fmt.Printf("源切片: %v\n", source)
fmt.Printf("目标切片: %v\n", dest)
// 部分复制
partial := make([]int, 3)
copy(partial, source[1:4])
fmt.Printf("部分复制: %v\n", partial)
// 3. 删除操作(通过切片重组实现)
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("原始切片: %v\n", numbers)
// 删除索引为3的元素(值为4)
index := 3
numbers = append(numbers[:index], numbers[index+1:]...)
fmt.Printf("删除索引3后: %v\n", numbers)
// 删除前两个元素
numbers = numbers[2:]
fmt.Printf("删除前两个元素: %v\n", numbers)
// 删除最后一个元素
numbers = numbers[:len(numbers)-1]
fmt.Printf("删除最后一个元素: %v\n", numbers)
}
切片的内存管理 #
package main
import "fmt"
func main() {
// 1. 切片扩容机制演示
slice := make([]int, 0, 1)
fmt.Printf("初始: 长度=%d, 容量=%d\n", len(slice), cap(slice))
for i := 0; i < 10; i++ {
slice = append(slice, i)
fmt.Printf("添加 %d: 长度=%d, 容量=%d\n",
i, len(slice), cap(slice))
}
// 2. 切片共享底层数组的问题
original := []int{1, 2, 3, 4, 5}
slice1 := original[1:3] // [2, 3]
slice2 := original[2:4] // [3, 4]
fmt.Printf("原始切片: %v\n", original)
fmt.Printf("切片1: %v\n", slice1)
fmt.Printf("切片2: %v\n", slice2)
// 修改 slice1 会影响原始切片和 slice2
slice1[1] = 99
fmt.Printf("修改 slice1[1] 后:\n")
fmt.Printf("原始切片: %v\n", original)
fmt.Printf("切片1: %v\n", slice1)
fmt.Printf("切片2: %v\n", slice2)
// 3. 避免共享问题的方法
safe := make([]int, len(slice1))
copy(safe, slice1)
safe[0] = 888
fmt.Printf("安全复制后修改:\n")
fmt.Printf("原始切片: %v\n", original)
fmt.Printf("安全切片: %v\n", safe)
}
数组与切片的比较 #
性能和使用场景 #
package main
import (
"fmt"
"time"
)
// 数组作为参数(值传递)
func processArray(arr [1000]int) int {
sum := 0
for _, v := range arr {
sum += v
}
return sum
}
// 切片作为参数(引用传递)
func processSlice(slice []int) int {
sum := 0
for _, v := range slice {
sum += v
}
return sum
}
func main() {
// 1. 内存使用比较
arr := [1000]int{}
slice := make([]int, 1000)
// 填充数据
for i := 0; i < 1000; i++ {
arr[i] = i
slice[i] = i
}
// 2. 性能测试
start := time.Now()
for i := 0; i < 10000; i++ {
processArray(arr)
}
arrayTime := time.Since(start)
start = time.Now()
for i := 0; i < 10000; i++ {
processSlice(slice)
}
sliceTime := time.Since(start)
fmt.Printf("数组处理时间: %v\n", arrayTime)
fmt.Printf("切片处理时间: %v\n", sliceTime)
// 3. 使用场景建议
fmt.Println("\n使用场景建议:")
fmt.Println("数组适用于:")
fmt.Println("- 固定大小的数据集合")
fmt.Println("- 需要高性能的场景")
fmt.Println("- 作为其他数据结构的基础")
fmt.Println("\n切片适用于:")
fmt.Println("- 动态大小的数据集合")
fmt.Println("- 需要灵活操作的场景")
fmt.Println("- 大部分日常编程需求")
}
实际应用示例 #
数据处理和算法实现 #
package main
import (
"fmt"
"sort"
)
// 学生成绩管理系统
type Student struct {
Name string
Score int
}
func main() {
// 1. 成绩统计
scores := []int{85, 92, 78, 96, 88, 76, 94, 82, 90, 87}
fmt.Printf("原始成绩: %v\n", scores)
// 计算平均分
sum := 0
for _, score := range scores {
sum += score
}
average := float64(sum) / float64(len(scores))
fmt.Printf("平均分: %.2f\n", average)
// 找出最高分和最低分
maxScore := scores[0]
minScore := scores[0]
for _, score := range scores {
if score > maxScore {
maxScore = score
}
if score < minScore {
minScore = score
}
}
fmt.Printf("最高分: %d, 最低分: %d\n", maxScore, minScore)
// 2. 成绩分级
gradeCount := make(map[string]int)
for _, score := range scores {
switch {
case score >= 90:
gradeCount["优秀"]++
case score >= 80:
gradeCount["良好"]++
case score >= 70:
gradeCount["中等"]++
case score >= 60:
gradeCount["及格"]++
default:
gradeCount["不及格"]++
}
}
fmt.Println("成绩分布:")
for grade, count := range gradeCount {
fmt.Printf("%s: %d人\n", grade, count)
}
// 3. 学生排序
students := []Student{
{"张三", 85},
{"李四", 92},
{"王五", 78},
{"赵六", 96},
{"钱七", 88},
}
fmt.Println("\n学生成绩排序:")
fmt.Println("排序前:")
for _, student := range students {
fmt.Printf("%s: %d分\n", student.Name, student.Score)
}
// 按成绩降序排序
sort.Slice(students, func(i, j int) bool {
return students[i].Score > students[j].Score
})
fmt.Println("排序后:")
for i, student := range students {
fmt.Printf("第%d名 %s: %d分\n", i+1, student.Name, student.Score)
}
}
矩阵运算 #
package main
import "fmt"
// 矩阵类型定义
type Matrix [][]int
// 创建矩阵
func NewMatrix(rows, cols int) Matrix {
matrix := make(Matrix, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}
return matrix
}
// 矩阵加法
func (m Matrix) Add(other Matrix) Matrix {
if len(m) != len(other) || len(m[0]) != len(other[0]) {
panic("矩阵维度不匹配")
}
result := NewMatrix(len(m), len(m[0]))
for i := 0; i < len(m); i++ {
for j := 0; j < len(m[0]); j++ {
result[i][j] = m[i][j] + other[i][j]
}
}
return result
}
// 矩阵乘法
func (m Matrix) Multiply(other Matrix) Matrix {
if len(m[0]) != len(other) {
panic("矩阵维度不匹配")
}
result := NewMatrix(len(m), len(other[0]))
for i := 0; i < len(m); i++ {
for j := 0; j < len(other[0]); j++ {
for k := 0; k < len(other); k++ {
result[i][j] += m[i][k] * other[k][j]
}
}
}
return result
}
// 打印矩阵
func (m Matrix) Print() {
for _, row := range m {
for _, val := range row {
fmt.Printf("%3d ", val)
}
fmt.Println()
}
}
func main() {
// 创建矩阵
matrix1 := Matrix{
{1, 2, 3},
{4, 5, 6},
}
matrix2 := Matrix{
{7, 8, 9},
{10, 11, 12},
}
matrix3 := Matrix{
{1, 2},
{3, 4},
{5, 6},
}
fmt.Println("矩阵1:")
matrix1.Print()
fmt.Println("矩阵2:")
matrix2.Print()
fmt.Println("矩阵3:")
matrix3.Print()
// 矩阵加法
fmt.Println("矩阵1 + 矩阵2:")
sum := matrix1.Add(matrix2)
sum.Print()
// 矩阵乘法
fmt.Println("矩阵1 × 矩阵3:")
product := matrix1.Multiply(matrix3)
product.Print()
}
动态数组实现 #
package main
import "fmt"
// 动态数组结构
type DynamicArray struct {
data []int
size int
capacity int
}
// 创建动态数组
func NewDynamicArray(capacity int) *DynamicArray {
return &DynamicArray{
data: make([]int, capacity),
size: 0,
capacity: capacity,
}
}
// 添加元素
func (da *DynamicArray) Add(value int) {
if da.size >= da.capacity {
da.resize()
}
da.data[da.size] = value
da.size++
}
// 扩容
func (da *DynamicArray) resize() {
newCapacity := da.capacity * 2
newData := make([]int, newCapacity)
copy(newData, da.data)
da.data = newData
da.capacity = newCapacity
fmt.Printf("数组扩容到: %d\n", newCapacity)
}
// 获取元素
func (da *DynamicArray) Get(index int) int {
if index < 0 || index >= da.size {
panic("索引越界")
}
return da.data[index]
}
// 删除元素
func (da *DynamicArray) Remove(index int) {
if index < 0 || index >= da.size {
panic("索引越界")
}
for i := index; i < da.size-1; i++ {
da.data[i] = da.data[i+1]
}
da.size--
}
// 获取大小
func (da *DynamicArray) Size() int {
return da.size
}
// 打印数组
func (da *DynamicArray) Print() {
fmt.Printf("数组内容: [")
for i := 0; i < da.size; i++ {
fmt.Printf("%d", da.data[i])
if i < da.size-1 {
fmt.Printf(", ")
}
}
fmt.Printf("], 大小: %d, 容量: %d\n", da.size, da.capacity)
}
func main() {
// 创建动态数组
arr := NewDynamicArray(2)
// 添加元素
for i := 1; i <= 10; i++ {
arr.Add(i * 10)
arr.Print()
}
// 访问元素
fmt.Printf("索引2的元素: %d\n", arr.Get(2))
// 删除元素
fmt.Println("删除索引1的元素:")
arr.Remove(1)
arr.Print()
// 继续添加元素
arr.Add(999)
arr.Print()
}
最佳实践和注意事项 #
性能优化建议 #
package main
import (
"fmt"
"time"
)
func main() {
// 1. 预分配切片容量
fmt.Println("切片容量预分配的重要性:")
// 不好的做法:频繁扩容
start := time.Now()
var badSlice []int
for i := 0; i < 100000; i++ {
badSlice = append(badSlice, i)
}
badTime := time.Since(start)
// 好的做法:预分配容量
start = time.Now()
goodSlice := make([]int, 0, 100000)
for i := 0; i < 100000; i++ {
goodSlice = append(goodSlice, i)
}
goodTime := time.Since(start)
fmt.Printf("未预分配时间: %v\n", badTime)
fmt.Printf("预分配时间: %v\n", goodTime)
fmt.Printf("性能提升: %.2fx\n", float64(badTime)/float64(goodTime))
// 2. 避免切片内存泄漏
fmt.Println("\n避免切片内存泄漏:")
// 问题:保留大切片的小部分
bigSlice := make([]int, 1000000)
for i := range bigSlice {
bigSlice[i] = i
}
// 不好的做法:直接切片,会保持对整个底层数组的引用
badSubSlice := bigSlice[0:10]
// 好的做法:复制需要的部分
goodSubSlice := make([]int, 10)
copy(goodSubSlice, bigSlice[0:10])
fmt.Printf("不好的子切片长度: %d, 容量: %d\n",
len(badSubSlice), cap(badSubSlice))
fmt.Printf("好的子切片长度: %d, 容量: %d\n",
len(goodSubSlice), cap(goodSubSlice))
// 3. 正确的切片删除方法
fmt.Println("\n切片删除的最佳实践:")
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Printf("原始切片: %v\n", numbers)
// 删除中间元素的高效方法
index := 4 // 删除值为5的元素
numbers = append(numbers[:index], numbers[index+1:]...)
fmt.Printf("删除索引4后: %v\n", numbers)
// 如果不需要保持顺序,可以用更高效的方法
numbers2 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
index = 4
numbers2[index] = numbers2[len(numbers2)-1] // 用最后一个元素覆盖
numbers2 = numbers2[:len(numbers2)-1] // 缩短切片
fmt.Printf("快速删除后: %v\n", numbers2)
}
小结 #
本节详细介绍了 Go 语言中的数组和切片,主要内容包括:
数组特点 #
- 固定长度:编译时确定大小,长度是类型的一部分
- 值类型:赋值和传参时会复制整个数组
- 高性能:直接存储在栈上,访问速度快
- 适用场景:固定大小的数据集合,对性能要求高的场景
切片特点 #
- 动态长度:可以动态增长和缩减
- 引用类型:指向底层数组,传参时只复制切片头
- 灵活操作:支持 append、copy 等丰富的操作
- 适用场景:大部分日常编程需求,需要动态调整大小的数据集合
最佳实践 #
- 优先使用切片而不是数组
- 预分配切片容量以提高性能
- 注意切片的内存泄漏问题
- 理解切片的底层机制,避免意外的数据共享
性能考虑 #
- 数组传参会复制整个数组,切片传参只复制切片头
- 切片扩容时会重新分配内存,预分配可以避免频繁扩容
- 大切片的小部分引用可能导致内存泄漏
掌握数组和切片的使用是 Go 语言编程的基础,它们是构建更复杂数据结构和算法的重要工具。
练习题:
- 实现一个函数,找出数组中的第二大元素
- 编写程序实现矩阵的转置操作
- 实现一个动态栈,支持 push、pop 和 peek 操作
- 编写函数合并两个有序切片为一个有序切片
- 实现一个环形缓冲区,支持读写操作