【Go】--数组和切片

文章目录

Go 语言数组与切片

数组和切片

数组和切片是 Go 语言中处理集合数据的核心数据结构。数组提供固定大小的存储,切片提供动态灵活的接口。理解它们的区别、联系和适用场景,对于编写高效、正确的 Go 程序至关重要。

  • 数组:适合固定大小、性能敏感的场景
  • 切片:适合动态大小、需要灵活操作的场景
  • 选择原则:根据数据大小是否固定、是否需要动态调整来选择

数组 (Array)

定义

数组是具有固定长度的相同类型元素的序列。数组的长度是类型的一部分,因此不同长度的数组是不同的类型。

语法结构

go 复制代码
// 声明语法
var arrayName [length]elementType

// 初始化语法
var arrayName [length]elementType = [length]elementType{value1, value2, ..., valueN}
arrayName := [length]elementType{value1, value2, ..., valueN}

// 自动推断长度
arrayName := [...]elementType{value1, value2, ..., valueN}

特性

  1. 固定长度:数组长度在编译时确定,不可改变
  2. 值类型:数组是值类型,赋值和传参会复制整个数组
  3. 类型包含长度[3]int[5]int 是不同的类型
  4. 内存连续:数组元素在内存中连续存储
go 复制代码
package main

import "fmt"

func main() {
    // 声明数组
    var arr1 [3]int
    fmt.Println("arr1:", arr1) // [0 0 0]
    
    // 初始化数组
    var arr2 [3]int = [3]int{1, 2, 3}
    arr3 := [3]int{4, 5, 6}
    fmt.Println("arr2:", arr2) // [1 2 3]
    fmt.Println("arr3:", arr3) // [4 5 6]
    
    // 自动推断长度
    arr4 := [...]int{7, 8, 9, 10}
    fmt.Println("arr4:", arr4)        // [7 8 9 10]
    fmt.Println("arr4 长度:", len(arr4)) // 4
    
    // 指定索引初始化
    arr5 := [5]int{1: 10, 3: 30}
    fmt.Println("arr5:", arr5) // [0 10 0 30 0]
    
    // 多维数组
    var matrix [2][3]int = [2][3]int{
        {1, 2, 3},
        {4, 5, 6},
    }
    fmt.Println("matrix:", matrix) // [[1 2 3] [4 5 6]]
}

常见操作

go 复制代码
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    
    // 访问元素
    fmt.Println("第一个元素:", arr[0]) // 10
    fmt.Println("最后一个元素:", arr[len(arr)-1]) // 50
    
    // 修改元素
    arr[0] = 100
    fmt.Println("修改后:", arr) // [100 20 30 40 50]
    
    // 遍历数组
    fmt.Println("遍历数组:")
    for i := 0; i < len(arr); i++ {
        fmt.Printf("arr[%d] = %d\n", i, arr[i])
    }
    
    // 使用 range 遍历
    fmt.Println("使用 range 遍历:")
    for index, value := range arr {
        fmt.Printf("索引: %d, 值: %d\n", index, value)
    }
    
    // 数组比较(只有相同长度和类型的数组可以比较)
    arr1 := [3]int{1, 2, 3}
    arr2 := [3]int{1, 2, 3}
    arr3 := [3]int{1, 2, 4}
    fmt.Println("arr1 == arr2:", arr1 == arr2) // true
    fmt.Println("arr1 == arr3:", arr1 == arr3) // false
}

切片 (Slice)

定义

切片是对数组的抽象,提供了更灵活、更强大的序列接口。切片是引用类型,底层引用一个数组。

语法结构

go 复制代码
// 声明切片
var sliceName []elementType

// 使用 make 创建
sliceName := make([]elementType, length, capacity)

// 字面量创建
sliceName := []elementType{value1, value2, ..., valueN}

// 从数组创建
sliceName := arrayName[start:end]

内部结构

切片包含三个组件:

  • 指针:指向底层数组的起始位置
  • 长度:切片中元素的数量
  • 容量:从切片起始位置到底层数组末尾的元素数量
go 复制代码
package main

import "fmt"

func main() {
    // 声明切片
    var slice1 []int
    fmt.Println("slice1:", slice1)        // []
    fmt.Println("slice1 是否为 nil:", slice1 == nil) // true
    
    // 使用 make 创建切片
    slice2 := make([]int, 3, 5)
    fmt.Println("slice2:", slice2)        // [0 0 0]
    fmt.Println("长度:", len(slice2))      // 3
    fmt.Println("容量:", cap(slice2))      // 5
    
    // 字面量创建
    slice3 := []int{1, 2, 3, 4, 5}
    fmt.Println("slice3:", slice3)        // [1 2 3 4 5]
    
    // 从数组创建切片
    arr := [5]int{10, 20, 30, 40, 50}
    slice4 := arr[1:4] // 包含索引1,不包含索引4
    fmt.Println("slice4:", slice4)        // [20 30 40]
    
    // 修改切片会影响底层数组
    slice4[0] = 200
    fmt.Println("修改后数组:", arr)        // [10 200 30 40 50]
    fmt.Println("修改后切片:", slice4)      // [200 30 40]
}

常见操作

go 复制代码
package main

import "fmt"

func main() {
    // 创建切片
    slice := []int{1, 2, 3}
    
    // 添加元素
    slice = append(slice, 4)
    fmt.Println("添加后:", slice) // [1 2 3 4]
    
    // 添加多个元素
    slice = append(slice, 5, 6, 7)
    fmt.Println("添加多个后:", slice) // [1 2 3 4 5 6 7]
    
    // 合并切片
    slice2 := []int{8, 9, 10}
    slice = append(slice, slice2...)
    fmt.Println("合并后:", slice) // [1 2 3 4 5 6 7 8 9 10]
    
    // 复制切片
    slice3 := make([]int, len(slice))
    copy(slice3, slice)
    fmt.Println("复制后:", slice3) // [1 2 3 4 5 6 7 8 9 10]
    
    // 修改复制后的切片不会影响原切片
    slice3[0] = 100
    fmt.Println("原切片:", slice)   // [1 2 3 4 5 6 7 8 9 10]
    fmt.Println("新切片:", slice3) // [100 2 3 4 5 6 7 8 9 10]
    
    // 切片操作
    fmt.Println("前3个元素:", slice[:3])   // [1 2 3]
    fmt.Println("从第3个开始:", slice[3:])   // [4 5 6 7 8 9 10]
    fmt.Println("第2-5个元素:", slice[2:5]) // [3 4 5]
    
    // 删除元素(删除索引2的元素)
    slice = append(slice[:2], slice[3:]...)
    fmt.Println("删除后:", slice) // [1 2 4 5 6 7 8 9 10]
}

切片扩容机制

go 复制代码
package main

import "fmt"

func main() {
    slice := make([]int, 0, 2)
    fmt.Printf("初始 - 长度: %d, 容量: %d\n", len(slice), cap(slice))
    
    for i := 1; i <= 10; i++ {
        slice = append(slice, i)
        fmt.Printf("添加 %d - 长度: %d, 容量: %d\n", i, len(slice), cap(slice))
    }
    
    /* 输出示例:
    初始 - 长度: 0, 容量: 2
    添加 1 - 长度: 1, 容量: 2
    添加 2 - 长度: 2, 容量: 2
    添加 3 - 长度: 3, 容量: 4  (扩容)
    添加 4 - 长度: 4, 容量: 4
    添加 5 - 长度: 5, 容量: 8  (扩容)
    添加 6 - 长度: 6, 容量: 8
    添加 7 - 长度: 7, 容量: 8
    添加 8 - 长度: 8, 容量: 8
    添加 9 - 长度: 9, 容量: 16 (扩容)
    添加 10 - 长度: 10, 容量: 16
    */
}

数组与切片的区别与联系

主要区别

特性 数组 (Array) 切片 (Slice)
长度 固定,编译时确定 动态,运行时可变
类型 值类型 引用类型
传递 值传递(复制整个数组) 引用传递(传递切片头)
比较 可以比较(相同类型) 不能直接比较
声明 var arr [3]int var slice []int
零值 对应类型的零值数组 nil

联系

  1. 底层关系:切片基于数组实现,切片是数组的视图
  2. 内存共享:多个切片可以共享同一个底层数组
  3. 相互转换:数组可以转换为切片,但切片不能直接转换为数组

相互转换示例

go 复制代码
package main

import "fmt"

func main() {
    // 数组转切片
    arr := [5]int{1, 2, 3, 4, 5}
    slice := arr[:] // 整个数组转为切片
    fmt.Println("数组转切片:", slice) // [1 2 3 4 5]
    
    // 切片转数组(需要确保长度匹配)
    if len(slice) == 5 {
        var newArr [5]int
        copy(newArr[:], slice) // 将切片内容复制到数组
        fmt.Println("切片转数组:", newArr) // [1 2 3 4 5]
    }
    
    // Go 1.17+ 支持直接转换
    // newArr := (*[5]int)(slice) // 注意:需要长度匹配
}

使用场景

数组的使用场景

  1. 固定大小的集合:如月份、星期等固定长度的数据
  2. 性能敏感场景:数组在栈上分配,访问速度快
  3. 内存布局控制:需要精确控制内存布局时
  4. 作为切片底层:为切片提供底层存储
go 复制代码
package main

import "fmt"

func main() {
    // 固定大小的数据集合
    daysOfWeek := [7]string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"}
    
    // 矩阵运算
    var matrix1 [2][2]int = [2][2]int{{1, 2}, {3, 4}}
    var matrix2 [2][2]int = [2][2]int{{5, 6}, {7, 8}}
    
    // 性能敏感的场景
    var buffer [1024]byte // 固定大小的缓冲区
    
    fmt.Println("星期:", daysOfWeek)
    fmt.Println("矩阵1:", matrix1)
    fmt.Println("矩阵2:", matrix2)
    fmt.Println("缓冲区大小:", len(buffer))
}

切片的使用场景

  1. 动态集合:大小不确定的数据集合
  2. 函数参数:作为函数参数传递集合数据
  3. 数据流处理:处理来自网络、文件的数据流
  4. 字符串处理:字符串本质上是字节切片
go 复制代码
package main

import (
    "fmt"
    "strings"
)

func processData(data []int) []int {
    result := make([]int, 0, len(data))
    for _, value := range data {
        if value%2 == 0 {
            result = append(result, value*2)
        }
    }
    return result
}

func main() {
    // 动态数据处理
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    processed := processData(data)
    fmt.Println("处理后的数据:", processed) // [4 8 12 16 20]
    
    // 字符串处理(字符串可视为只读的字节切片)
    str := "Hello, 世界"
    bytes := []byte(str)
    runes := []rune(str)
    
    fmt.Println("字符串:", str)
    fmt.Println("字节切片:", bytes)
    fmt.Println("字符切片:", runes)
    fmt.Println("字符数量:", len(runes)) // 8 (Hello, 和 世界)
    
    // 字符串分割
    words := strings.Split("Go is a programming language", " ")
    fmt.Println("分割后的单词:", words)
}

高级特性

切片的内存优化

go 复制代码
package main

import "fmt"

func main() {
    // 预分配容量避免频繁扩容
    slice := make([]int, 0, 1000) // 预分配足够容量
    
    for i := 0; i < 1000; i++ {
        slice = append(slice, i)
    }
    
    fmt.Printf("最终长度: %d, 容量: %d\n", len(slice), cap(slice))
    
    // 切片截取可能导致内存泄漏
    bigSlice := make([]int, 10000)
    smallSlice := bigSlice[:10] // smallSlice 仍然引用整个 bigSlice
    
    // 正确的做法:复制需要的部分
    properSlice := make([]int, 10)
    copy(properSlice, bigSlice[:10])
    
    fmt.Printf("smallSlice 容量: %d\n", cap(smallSlice))     // 10000
    fmt.Printf("properSlice 容量: %d\n", cap(properSlice))   // 10
}

多维切片

go 复制代码
package main

import "fmt"

func main() {
    // 创建二维切片
    matrix := make([][]int, 3)
    for i := range matrix {
        matrix[i] = make([]int, 3)
        for j := range matrix[i] {
            matrix[i][j] = i*3 + j + 1
        }
    }
    
    fmt.Println("二维切片:")
    for i, row := range matrix {
        fmt.Printf("第%d行: %v\n", i, row)
    }
    
    // 不规则多维切片
    irregular := [][]int{
        {1},
        {2, 3},
        {4, 5, 6},
    }
    
    fmt.Println("\n不规则二维切片:")
    for i, row := range irregular {
        fmt.Printf("第%d行: %v\n", i, row)
    }
}

注意事项

数组注意事项

  1. 长度是类型的一部分:不同长度的数组不能相互赋值
  2. 值传递开销:大数组作为参数传递时会产生复制开销
  3. 零值初始化:声明但未初始化的数组会用零值填充
go 复制代码
package main

import "fmt"

func main() {
    // 错误:不同长度的数组不能赋值
    // var arr1 [3]int
    // var arr2 [5]int
    // arr1 = arr2 // 编译错误
    
    // 正确:使用相同类型的数组
    var arr3 [3]int = [3]int{1, 2, 3}
    var arr4 [3]int
    arr4 = arr3 // 正确
    fmt.Println("arr4:", arr4)
}

切片注意事项

  1. nil 切片与空切片:nil 切片没有底层数组,空切片有底层数组但长度为0
  2. 切片共享底层数组:修改切片可能影响其他共享同一底层数组的切片
  3. append 的副作用:append 可能返回新的切片引用
  4. 容量与长度的区别:容量 >= 长度
go 复制代码
package main

import "fmt"

func main() {
    // nil 切片 vs 空切片
    var nilSlice []int          // nil 切片
    emptySlice := []int{}        // 空切片
    emptySlice2 := make([]int, 0) // 空切片
    
    fmt.Printf("nilSlice: len=%d, cap=%d, nil=%t\n", 
        len(nilSlice), cap(nilSlice), nilSlice == nil) // len=0, cap=0, nil=true
    fmt.Printf("emptySlice: len=%d, cap=%d, nil=%t\n", 
        len(emptySlice), cap(emptySlice), emptySlice == nil) // len=0, cap=0, nil=false
    fmt.Printf("emptySlice2: len=%d, cap=%d, nil=%t\n", 
        len(emptySlice2), cap(emptySlice2), emptySlice2 == nil) // len=0, cap=0, nil=false
    
    // 切片共享问题
    arr := [5]int{1, 2, 3, 4, 5}
    slice1 := arr[1:4] // [2, 3, 4]
    slice2 := slice1[0:2] // [2, 3]
    
    slice2[0] = 200
    fmt.Println("arr:", arr)     // [1 200 3 4 5]
    fmt.Println("slice1:", slice1) // [200 3 4]
    fmt.Println("slice2:", slice2) // [200 3]
    
    // append 的副作用
    slice3 := make([]int, 2, 3)
    slice3[0], slice3[1] = 1, 2
    slice4 := append(slice3, 3)
    
    slice4[0] = 100
    fmt.Println("slice3:", slice3) // [1 2] (未受影响,因为容量足够)
    fmt.Println("slice4:", slice4) // [100 2 3]
    
    slice5 := append(slice3, 3, 4) // 超出容量,创建新底层数组
    slice5[0] = 200
    fmt.Println("slice3:", slice3) // [1 2] (未受影响)
    fmt.Println("slice5:", slice5) // [200 2 3 4]
}

ALL

  1. 预分配容量 :使用 make([]T, 0, capacity) 预分配足够容量
  2. 避免不必要的复制:对于大切片,考虑使用指针或引用
  3. 使用数组作为切片底层:对于固定大小的数据,先创建数组再转为切片
  4. 批量操作 :使用 copy 函数进行批量复制
相关推荐
-睡到自然醒~3 小时前
提升应用性能:Go中的同步与异步处理
开发语言·后端·golang
只吃不吃香菜3 小时前
Go WebSocket 协程泄漏问题分析与解决方案
开发语言·websocket·golang
文心快码BaiduComate3 小时前
新手该如何选择AI编程工具?文心快码Comate全方位体验
前端·后端·程序员
阿挥的编程日记3 小时前
基于SpringBoot的影评管理系统
java·spring boot·后端
java坤坤4 小时前
Spring Boot 集成 SpringDoc OpenAPI(Swagger)实战:从配置到接口文档落地
java·spring boot·后端
ChineHe4 小时前
Golang并发编程篇001_并发编程相关概念解释
开发语言·后端·golang
自由的疯4 小时前
java 怎么判断事务有无提交成功
java·后端·架构
bcbnb4 小时前
Socket 抓包工具与实战,从抓取到定位(Socket 的命令、分析)
后端
用户8356290780514 小时前
用Python轻松转换Excel表格为HTML格式
后端·python