golang-基础-Go语言数组
Go语言中的数组是一种固定长度的线性数据结构,所有元素必须是相同类型,且数组的长度在定义后不可更改。Go语言数组的详细讲解,包含定义、初始化、操作、特性及代码示例。
一、数组的定义
数组的定义格式为:
var 数组名 [长度]元素类型
- 数组的长度:必须是常量表达式(如整数常量或
const
定义的常量),且长度是数组类型的一部分。 - 元素类型:可以是任何基本类型(如
int
、string
)或自定义类型(如结构体)。
示例:定义数组
// 定义一个长度为5的int类型数组
var numbers [5]int
// 定义一个长度为3的字符串数组
var names [3]string
二、数组的初始化
Go语言提供了多种初始化数组的方式:
1. 显式初始化
// 初始化数组元素
var numbers = [5]int{1, 2, 3, 4, 5}
fmt.Println(numbers) // 输出: [1 2 3 4 5]
2. 隐式初始化
未指定的元素会自动初始化为对应类型的零值(如int
的零值是0
,string
的零值是空字符串)。
var numbers = [5]int{1, 2} // 剩余元素初始化为0
fmt.Println(numbers) // 输出: [1 2 0 0 0]
3. 自动推断长度
使用...
让编译器根据初始化元素的数量自动推断数组长度:
var numbers = [...]int{1, 2, 3, 4, 5}
fmt.Println(len(numbers)) // 输出: 5
4. 指定索引初始化
可以通过索引直接初始化特定位置的元素:
var sparseArray = [...]int{1: 10, 3: 30}
fmt.Println(sparseArray) // 输出: [0 10 0 30]
5. 多维数组初始化
// 定义一个2x3的二维数组
var matrix [2][3]int = [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrix) // 输出: [[1 2 3] [4 5 6]]
三、数组的操作
1. 访问数组元素
通过下标访问数组元素,下标从0
开始,最大值为len(array)-1
。越界访问会触发运行时错误(panic)。
numbers := [5]int{10, 20, 30, 40, 50}
fmt.Println(numbers[0]) // 输出: 10
fmt.Println(numbers[4]) // 输出: 50
2. 修改数组元素
numbers := [5]int{10, 20, 30, 40, 50}
numbers[2] = 100 // 修改索引为2的元素
fmt.Println(numbers) // 输出: [10 20 100 40 50]
3. 遍历数组
-
普通
for
循环:numbers := [5]int{1, 2, 3, 4, 5} for i := 0; i < len(numbers); i++ { fmt.Println("Index:", i, "Value:", numbers[i]) }
-
range
关键字:numbers := [5]int{1, 2, 3, 4, 5} for index, value := range numbers { fmt.Println("Index:", index, "Value:", value) }
四、数组的特性
1. 数组是值类型
- 赋值:数组赋值会复制整个数组。
- 传参:将数组作为函数参数传递时,会复制整个数组(而非引用)。
示例:数组赋值与传参
func modifyArray(arr [3]int) {
arr[0] = 100
fmt.Println("Inside function:", arr)
}
func main() {
arr := [3]int{1, 2, 3}
modifyArray(arr) // 传入的是数组的副本
fmt.Println("Outside function:", arr) // 输出: [1 2 3]
}
2. 数组的比较
Go语言允许直接使用==
或!=
比较两个数组是否相等,前提是它们的类型和长度相同。
arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
arr3 := [3]int{1, 2, 4}
fmt.Println(arr1 == arr2) // 输出: true
fmt.Println(arr1 == arr3) // 输出: false
在 Go 语言中,如果两个数组的 类型或长度不同,无法直接使用 ==
或 !=
进行比较(因为这会触发编译错误)。此时需要通过其他方式间接比较,具体取决于你的需求。
1. 手动比较元素(适用于类型或长度不同的数组)
如果两个数组的元素类型相同但长度不同,或者类型不同(例如 [3]int
和 [5]int
),可以通过以下步骤比较:
步骤:
- 检查类型和长度是否匹配:如果不匹配,直接返回
false
。 - 逐元素比较:如果类型和长度匹配,逐个比较元素;如果不匹配,可以选择忽略长度差异或截断处理。
示例代码:
func compareArrays(arr1, arr2 interface{}) bool {
// 使用反射获取数组类型和值
v1 := reflect.ValueOf(arr1)
v2 := reflect.ValueOf(arr2)
// 检查是否为数组类型
if v1.Kind() != reflect.Array || v2.Kind() != reflect.Array {
return false
}
// 检查元素类型是否相同
if v1.Type().Elem() != v2.Type().Elem() {
return false
}
// 获取较短的数组长度
minLen := v1.Len()
if v2.Len() < minLen {
minLen = v2.Len()
}
// 逐元素比较
for i := 0; i < minLen; i++ {
if !reflect.DeepEqual(v1.Index(i).Interface(), v2.Index(i).Interface()) {
return false
}
}
// 如果长度不同,返回 false
return v1.Len() == v2.Len()
}
使用示例:
arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
arr3 := [5]int{1, 2, 3, 4, 5}
fmt.Println(compareArrays(arr1, arr2)) // true
fmt.Println(compareArrays(arr1, arr3)) // false (类型相同但长度不同)
fmt.Println(compareArrays(arr1, [3]string{"a", "b", "c"})) // false (类型不同)
2. 使用 reflect.DeepEqual
(适用于任意类型数组)
如果数组的类型或长度不同,但你希望比较它们的元素值(忽略类型或长度差异),可以使用 reflect.DeepEqual
,但需要额外处理。
示例代码:
import "reflect"
func compareArraysDeepEqual(arr1, arr2 interface{}) bool {
// 检查是否为数组类型
v1 := reflect.ValueOf(arr1)
v2 := reflect.ValueOf(arr2)
if v1.Kind() != reflect.Array || v2.Kind() != reflect.Array {
return false
}
// 比较元素类型是否相同
if v1.Type().Elem() != v2.Type().Elem() {
return false
}
// 使用 DeepEqual 比较整个数组
return reflect.DeepEqual(arr1, arr2)
}
使用示例:
arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
arr3 := [3]int{1, 2, 4}
fmt.Println(compareArraysDeepEqual(arr1, arr2)) // true
fmt.Println(compareArraysDeepEqual(arr1, arr3)) // false
3. 转换为切片后比较(适用于动态长度场景)
如果数组的长度可能不同,但你希望比较它们的元素值(忽略长度差异),可以将数组转换为切片后再比较。
示例代码:
func compareArraySlices(arr1, arr2 interface{}) bool {
v1 := reflect.ValueOf(arr1)
v2 := reflect.ValueOf(arr2)
// 将数组转换为切片
slice1 := reflect.MakeSlice(reflect.SliceOf(v1.Type().Elem()), v1.Len(), v1.Len())
reflect.Copy(slice1, v1)
slice2 := reflect.MakeSlice(reflect.SliceOf(v2.Type().Elem()), v2.Len(), v2.Len())
reflect.Copy(slice2, v2)
// 比较切片
return reflect.DeepEqual(slice1.Interface(), slice2.Interface())
}
使用示例:
arr1 := [3]int{1, 2, 3}
arr2 := [5]int{1, 2, 3, 4, 5}
fmt.Println(compareArraySlices(arr1, arr2)) // false (元素值不完全匹配)
4. 自定义逻辑处理(根据业务需求)
如果数组的类型或长度不同,但你需要自定义比较逻辑(例如忽略长度差异、部分匹配等),可以编写特定的比较函数。
示例:忽略长度差异,比较前 N 个元素
func compareFirstNElements(arr1, arr2 interface{}, n int) bool {
v1 := reflect.ValueOf(arr1)
v2 := reflect.ValueOf(arr2)
// 检查是否为数组类型
if v1.Kind() != reflect.Array || v2.Kind() != reflect.Array {
return false
}
// 检查元素类型是否相同
if v1.Type().Elem() != v2.Type().Elem() {
return false
}
// 获取最小长度
minLen := v1.Len()
if v2.Len() < minLen {
minLen = v2.Len()
}
if n > minLen {
n = minLen
}
// 比较前 n 个元素
for i := 0; i < n; i++ {
if !reflect.DeepEqual(v1.Index(i).Interface(), v2.Index(i).Interface()) {
return false
}
}
return true
}
使用示例:
arr1 := [5]int{1, 2, 3, 4, 5}
arr2 := [3]int{1, 2, 3}
fmt.Println(compareFirstNElements(arr1, arr2, 3)) // true
总结
方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
手动比较元素 | 类型或长度不同的数组 | 灵活,可自定义逻辑 | 需要额外代码 |
reflect.DeepEqual | 类型相同的数组 | 简洁,直接比较所有元素 | 无法处理类型不同的数组 |
转换为切片后比较 | 动态长度场景 | 支持动态比较 | 性能开销较大 |
自定义逻辑(如忽略长度差异) | 业务需求特殊(如部分匹配) | 完全控制比较逻辑 | 实现复杂 |
注意事项
- 类型安全:如果数组的元素类型不同(如
[3]int
和[3]string
),比较时需确保类型一致,否则会触发运行时错误。 - 性能:反射(
reflect
包)的性能开销较高,不适合频繁调用。 - 切片 vs 数组:如果需要动态长度的数据结构,建议使用切片(
[]T
),但切片本身不能直接比较(需转换为数组或使用reflect.DeepEqual
)。
3. 获取数组的长度和容量
len(array)
:获取数组的长度(元素个数)。cap(array)
:对于数组来说,cap(array)
始终等于len(array)
,因为数组是固定长度的。
arr := [5]int{1, 2, 3, 4, 5}
fmt.Println("Length:", len(arr)) // 输出: 5
fmt.Println("Capacity:", cap(arr)) // 输出: 5
五、指针数组与数组指针
1. 指针数组
指针数组是一个数组,其元素是指针类型。
var ptrs [3]*int
x, y, z := 1, 2, 3
ptrs[0] = &x
ptrs[1] = &y
ptrs[2] = &z
for _, p := range ptrs {
fmt.Println(*p)
}
2. 数组指针
数组指针是指向数组的指针。
arr := [3]int{10, 20, 30}
var p *[3]int = &arr
fmt.Println(*p) // 输出: [10 20 30]
六、数组作为函数参数
由于数组是值类型,传递给函数时会复制整个数组。如果数组较大,频繁复制可能会影响性能。此时可以使用数组指针或切片(slice)优化。
示例:传递数组指针
func modifyArray(arr *[3]int) {
arr[0] = 100
}
func main() {
arr := [3]int{1, 2, 3}
modifyArray(&arr) // 传递数组指针
fmt.Println(arr) // 输出: [100 2 3]
}
七、多维数组
多维数组的定义和操作类似于一维数组,但需要通过多个下标访问元素。
示例:二维数组
// 定义一个3x3的二维数组
var matrix [3][3]int
// 填充数组
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
matrix[i][j] = i * j
}
}
// 打印数组
for _, row := range matrix {
fmt.Println(row)
}
八、数组与切片的区别
- 数组:长度固定,类型中包含长度(如
[5]int
)。 - 切片:长度可动态变化,基于数组实现,类型不包含长度(如
[]int
)。
示例:切片与数组的关系
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:3] // 创建切片,引用数组的第1到第2个元素
fmt.Println(slice) // 输出: [2 3]
slice[0] = 100
fmt.Println(arr) // 输出: [1 100 3 4 5](修改切片会影响底层数组)
九、总结
- 数组的优缺点:
- 优点:内存连续,访问速度快;适合固定长度的数据集合。
- 缺点:长度固定,灵活性差。
- 适用场景:
- 数据量小且长度固定的场景(如配置项、固定大小的缓冲区)。
- 需要直接操作底层内存的场景(如嵌入式开发)。
如果需要动态长度的集合,建议使用切片(slice)代替数组。
Go语言切片(Slice)的详细优缺点分析,以及与数组(Array) 的全面对比表格,深入理解两者的区别和适用场景。
✅Go语言切片(Slice)的优缺点
一、切片的定义
切片是对数组的动态抽象,它本身不存储数据,而是指向一个底层数组的引用结构,包含三个部分:
- 指针(
ptr
):指向底层数组的起始位置。 - 长度(
len
):当前切片中元素的个数。 - 容量(
cap
):从起始位置到底层数组末尾的元素个数。
s := []int{1, 2, 3} // 创建一个长度为3、容量为3的切片
二、切片的优缺点
-
优点:
1、动态长度:可使用
append
动态扩容,无需预先确定大小。2、内存高效:多个切片可共享同一底层数组,减少内存复制。
3、操作灵活:支持切片、截取、拼接等操作(如
s[1:3]
)。4、函数传参高效:传递的是结构体(指针+长度+容量),不会复制整个数据。
5、与标准库兼容性好:Go标准库中大多数集合操作都基于切片设计。
-
缺点:
1、共享底层数组:多个切片可能共享同一数组,修改一个可能影响其他切片(需注意“副作用”)。
2、扩容机制复杂:当容量不足时自动扩容,可能导致内存重新分配和数据复制(性能波动)。
3、不可比较(除与nil):不能直接用
==
比较两个切片是否相等(除非使用reflect.DeepEqual
或slices.Equal
)。4、内存泄漏风险:长时间持有小切片可能阻止整个大数组被GC回收(如
s = bigArr[100:101]
)
🔄数组 vs 切片 对比表格
特性 | 数组(Array) | 切片(Slice) |
---|---|---|
类型定义 | [N]T (如 [5]int ) | []T (如 []int ) |
长度 | 固定,定义时确定 | 动态,可变 |
是否值类型 | 是(赋值/传参会复制整个数组) | 否(本质是引用类型,但本身是结构体) |
内存布局 | 连续内存块,直接存储数据 | 结构体(ptr, len, cap),指向底层数组 |
初始化方式 | [3]int{1,2,3} 或 [...]int{1,2,3} | []int{1,2,3} 或 make([]int, 3) |
扩容能力 | 不支持 | 支持(通过 append 自动扩容) |
比较操作 | 可直接用 == / != (类型和长度相同) | 不能直接比较(除 nil ),需用 slices.Equal 或 reflect.DeepEqual |
函数传参效率 | 低(复制整个数组) | 高(只复制 slice header) |
共享数据 | 不共享(独立副本) | 可能共享底层数组(注意副作用) |
适用场景 | - 固定长度的小数据(如坐标 [2]float64 )- 需要值语义的场景 - 哈希表的 key(数组可作 key,切片不行) | - 动态数据集合(如列表、队列) - 函数参数传递 - 大多数日常开发场景 |
能否作为 map 的 key | ✅ 可以(如果元素类型可比较) | ❌ 不可以(切片不可比较) |
零值 | 空数组(如 [3]int 的零值是 [0 0 0] ) | nil (未初始化)或空切片 []T{} |
性能特点 | 访问快,栈上分配,无指针开销 | 稍有间接访问开销,但更灵活高效 |
🧪示例对比代码
package main
import (
"fmt"
"slices" // Go 1.21+ 提供的切片工具
)
func main() {
// --- 数组示例 ---
var arr1 [3]int = [3]int{1, 2, 3}
arr2 := arr1 // 赋值:复制整个数组
arr2[0] = 999
fmt.Println("arr1:", arr1) // [1 2 3]
fmt.Println("arr2:", arr2) // [999 2 3]
// 数组可以直接比较
fmt.Println("arr1 == arr2:", arr1 == arr2) // false
// --- 切片示例 ---
slice1 := []int{1, 2, 3}
slice2 := slice1 // 赋值:共享底层数组
slice2[0] = 999
fmt.Println("slice1:", slice1) // [999 2 3]
fmt.Println("slice2:", slice2) // [999 2 3]
// 切片不能直接比较
// fmt.Println(slice1 == slice2) // 编译错误!
// 使用 slices.Equal 比较
fmt.Println("slice1 == slice2:", slices.Equal(slice1, slice2)) // true
// 作为 map key
m := make(map[[3]int]string) // 数组可以作 key
// m := make(map[[]int]string) // 编译错误:切片不能作 key
}
✅总结建议
使用建议 | 说明 |
---|---|
优先使用切片 | 在绝大多数场景下(如函数参数、动态集合),应使用切片,它是 Go 的“一等公民”。 |
数组用于特殊场景 | 仅在需要固定长度、值语义、高性能或作为 map key 时使用数组。 |
避免切片内存泄漏 | 若从大数组截取小切片并长期持有,建议使用 copy 创建独立副本。 |
比较切片用 slices.Equal | Go 1.21+ 推荐使用 golang.org/x/exp/slices 或标准库 slices 包。 |
💡 一句话总结:
数组是“值”,切片是“视图”。
数组适合“小而固定”,切片适合“大而灵活”。
若做高性能或嵌入式开发,可以结合两者优势:用数组做底层存储,用切片做接口抽象。
- 感谢你赐予我前进的力量