当前位置: 首页 > news >正文

Golang语言基础篇003_数组、切片、map详解

数组、切片和映射(map)是Go语言中最常用的三种数据结构,它们各自有不同的特点和适用场景。掌握这些数据结构的使用方法对于编写高效的Go程序至关重要。

1. 数组(Array)

11. 数组的概念和特性

数组是Go语言中的一种基本数据结构,它是固定长度同类型元素序列。数组的特点包括:

  • 固定长度:数组一旦声明,其长度就不能改变。

    数组长度是数组类型的一部分,因此var a [10]intvar b [5] int是不同的数据类型。

  • 同质性:数组中的所有元素必须是相同类型。

  • 连续内存:数组元素在内存中连续存储。

  • 值类型:数组是值类型,赋值和传参时会复制整个数组。因此改变数组副本的值,不会影响原数组。

  • 可比较:支持 “==”、”!=” 操作符,因为内存总是被初始化过的,这里比较的是数组的内容。

1.2. 数组的声明和初始化

在Go语言中,数组有多种声明和初始化方式:

1.2.1. 声明数组

声明数组的方式如下:

// 声明一个长度为5的整数数组(零值初始化)
var arr1 [5]int// 声明并初始化数组
var arr2 [3]string = [3]string{"apple", "banana", "orange"}// 使用短变量声明
arr3 := [5]int{1, 2, 3, 4, 5}// 让编译器自动计算数组长度
arr4 := [...]int{1, 2, 3, 4, 5}  // 长度为5// 指定索引初始化
arr5 := [5]int{0: 10, 4: 50}  // 索引0为10,索引4为50,其余为0

注意:声明数组是,数组长度必须是常量,使用变量会报错。

length := 3 // length一个变量
c := [length]int{1, 2, 3} // 报错:declared and not used: length ; invalid array length lengthconst length = 3 // length是一个常量
c := [length]int{1, 2, 3} // 正确

1.2.2. 多维数组

多维数组的基本使用方式如下:

// 声明二维数组
var matrix [3][3]int// 初始化二维数组
matrix2 := [2][3]int{{1, 2, 3},{4, 5, 6},
}// 使用索引号初始化元素
var arr2 = [...][2]int{0: {1: 2},1: {1: 4},
}// 访问二维数组元素
value := matrix2[0][1]  // 第一行第二列的值

需要特别注意的是,多维数组声明时,只有第一个维度支持编译器自动计算长度:

// 第一个维度支持编译器自动计算长度
matrix2 := [...][3]int{{1, 2, 3},{4, 5, 6},
}// 其他维度不支持编译器自动计算长度
matrix2 := [2][...]int{{1, 2, 3},{4, 5, 6},
} // 报错:invalid use of [...] array (outside a composite literal)

1.3. 数组的操作

1.3.1. 访问和修改数组元素

arr := [5]int{1, 2, 3, 4, 5}// 访问元素
first := arr[0]  // 获取第一个元素
last := arr[4]   // 获取最后一个元素// 修改元素
arr[0] = 10  // 修改第一个元素// 获取数组长度
length := len(arr)  // 返回5

1.3.2. 数组遍历

arr := [5]int{1, 2, 3, 4, 5}// 使用索引遍历
for i := 0; i < len(arr); i++ {fmt.Printf("索引%d: %d\n", i, arr[i])
}// 使用range遍历(推荐)
for index, value := range arr {fmt.Printf("索引%d: %d\n", index, value)
}// 只获取值(忽略索引)
for _, value := range arr {fmt.Printf("值: %d\n", value)
}

1.3.3. 数组比较和复制

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// 数组赋值会复制整个数组
arr4 := arr1  // 复制arr1到arr4
arr4[0] = 10  // 修改arr4不会影响arr1
fmt.Println(arr1[0])  // 仍然是1

1.3.4. 数组的长度

内置函数 len 和 cap 都返回数组长度 (元素数量)。

// 声明并初始化数组
a := [...][2]int{{1, 2},{3, 4},{5, 6},
}
fmt.Println(len(a)) // 3
fmt.Println(len(a[0])) // 2
fmt.Println(cap(a)) // 3
fmt.Println(cap(a[0])) // 2

1.3.5. 数组截取

使用arr[m:n]的语法,可以截取数组。

// 声明一维数组
a := [6]int{1,2,3,4,5,6}
// 截取
fmt.Println(a[0:1]) // [1]
fmt.Println(a[3:]) // [4 5 6]
fmt.Println(a[:5]) // [1 2 3 4 5]

该方式同样适用于多维数组:

// 声明多维数组
a := [...][2]int{{1, 2},{3, 4},{5, 6},
}
// 截取
fmt.Println(a[0:2][0:1]) // [[1 2]]

特别注意: 截取数组得到的结果,是一个切片Slice,而非一个数组:

// 定义数组
a := [5]int{1, 2, 3, 4, 5}
// 数组截取
b := a[2:4]
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(b) // [3 4]
fmt.Printf("%T\n", a) // [5]int
fmt.Printf("%T\n", b) // []int

[]int是一个切片Slice,而非一个数组。关于切片后面会说…

2. 切片(Slice)

由于数组的长度固定,这在很多场景下不够灵活。例如:

// 这样的代码在实际开发中很少见
func processScores(scores [100]int) {// 只能处理恰好100个分数
}

为了解决这个问题,Go语言引入了切片(Slice)。

2.1. 切片的概念和特性

切片是对数组的封装,提供了动态数组的功能。切片的特点包括:

slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

  • 动态长度:可以根据需要增长或缩小
  • 引用类型:切片是引用类型,指向底层数组
  • 灵活性:支持追加、截取等操作

2.2. 切片的声明和初始化

2.2.1. 声明切片

切片的基本声明方式如下:

// 声明一个整数切片(零值为nil)
var slice1 []int// 使用make创建切片(虽然元素仍为零值,但切片已经初始化,不为nil)
slice2 := make([]int, 5)      // 长度为5,容量为5
slice3 := make([]int, 3, 5)   // 长度为3,容量为5

与数组一样,切片也可以在声明时初始化:

// 直接初始化
slice8 := []int{1, 2, 3, 4, 5}

2.2.2. 通过截取来创建切片

可以通过截取数组的方式来初始化切片:

// 定义数组
a := [5]int{1, 2, 3, 4, 5}
// 数组截取,以初始化切片
b := a[2:4]
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(b) // [3 4]
fmt.Printf("%T\n", a) // [5]int
fmt.Printf("%T\n", b) // []int

切片也支持截取操作,可以通过截取切片的方式来初始化另一个切片:

// 声明并初始化切片
a := []int{1, 2, 3, 4, 5}
// 切片截取,以初始化切片
b := a[2:4]
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(b) // [3 4]
fmt.Printf("%T\n", a) // []int
fmt.Printf("%T\n", b) // []int

2.2.3. 理解切片的内部结构

切片的底层是数组,切片包含三个字段:

  • 指向底层数组的指针
  • 长度(len)
  • 容量(cap)
slice := make([]int, 3, 5)
fmt.Println(len(slice))  // 3
fmt.Println(cap(slice))  // 5

由于长度和容量是切片的属性,因此获取长度和容量时不需要遍历底层数组,性能约等于访问一个普通变量。

2.3. 切片的操作

2.3.1. append追加元素

append 内置函数将元素追加到切片的末尾:

  • 如果目标切片有足够的容量,直接添加新元素。
  • 如果目标切片没有足够的容量,会重新分配一个底层数组,然后返回新的切片。因此有必要保存append的结果(通常是保存到切片原本的变量中)。
  • 作为一种特殊情况,将字符串append到字节切片是合法的。

追加单个元素:

slice := []int{1, 2, 3}// 追加单个元素
slice = append(slice, 4)fmt.Println(slice)  // [1 2 3 4]

追加多个元素:

slice := []int{1, 2, 3}// 追加多个元素
slice = append(slice, 5, 6, 7)
fmt.Println(slice)  // [1 2 3 5 6 7]

还可以追加另一个切片的元素:

slice := []int{1, 2, 3}// 追加另一个切片
other := []int{8, 9, 10}
slice = append(slice, other...)fmt.Println(slice)  // [1 2 3 8 9 10]

2.3.2. 切片截取

与数组一样,使用slice[m:n]的语法,可以截取切片:

// 声明一维切片
a := []int{1,2,3,4,5,6}
// 截取
fmt.Println(a[0:1]) // [1]
fmt.Println(a[3:]) // [4 5 6]
fmt.Println(a[:5]) // [1 2 3 4 5]

该方式同样适用于多维切片:

// 声明多维切片
a := [][]int{{1, 2},{3, 4},{5, 6},
}
// 截取
fmt.Println(a[0:2][0:1]) // [[1 2]]

特别注意: 截取数组得到的结果,是一个切片Slice,而非一个数组:

// 定义切片
a := []int{1, 2, 3, 4, 5}
// 切片截取
b := a[2:4]
fmt.Println(a) // [1 2 3 4 5]
fmt.Println(b) // [3 4]
fmt.Printf("%T\n", a) // []int
fmt.Printf("%T\n", b) // []int

另外,截取时还可以指定容量:

// 语法格式
slice := array[low:high:capacity]
  • low:起始索引(包含)
  • high:结束索引(不包含)
  • capacity:指定切片的容量

上述三个索引多不能超过原数组的长度。

新切片的长度与容量:

  • 长度是 high - low
  • 容量是 capacity - low
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}// 基本截取操作
part1 := slice[2:5]   // [3 4 5]
part2 := slice[:5]    // [1 2 3 4 5]
part3 := slice[5:]    // [6 7 8 9 10]
part4 := slice[:]     // 整个切片的副本// 指定容量的截取
part5 := slice[2:5:7] // 长度为3,容量为5(7-2)

特别注意:截取后得到的新切片,与原数组和切片共享底层数组内存,直到其中一个重新分配。

// 定义源切片
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
// 截取
b := a[:5]// 从0索引开始截取,由于共享内存,因此两个切片起始地址相同
fmt.Printf("%p\n", a) // 0x14000016500
fmt.Printf("%p\n", b) // 0x14000016500// 修改其中一个切片的元素,另一个切片的数组也会变(只针对共享内存区间的元素)
a[0] = 111
a[8] = 888
b[1] = 222
fmt.Println(a) // [111 222 3 4 5 6 7 8 888]
fmt.Println(b) // [111 222 3 4 5]// 追加元素以使b切片重新分配
for i := 0; i < 10; i++ {b = append(b, i)
}// 由于重新分配内存,b切片起始地址变更
fmt.Printf("%p\n", a) // 0x14000016500
fmt.Printf("%p\n", b) // 0x14000110000// 再次修改元素,两者不再互相影响
a[0] = 100
b[1] = 200
fmt.Println(a) // [100 222 3 4 5 0 1 2 3]
fmt.Println(b) // [111 200 3 4 5 0 1 2 3 4 5 6 7 8 9]

2.3.3. copy复制切片

内置函数copy可以用来复制切片:

src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))// 使用copy函数复制
n := copy(dst, src)
fmt.Printf("复制了%d个元素\n", n)
fmt.Println(dst)  // [1 2 3 4 5]

使用内置函数copy,比手动循环copy更加高效。

这里就不贴测试性能的代码了,读者可以自己去测试!

2.3.4. 切片遍历

切片的遍历方式与数组遍历一样:

  • 使用索引遍历
slice := []int{1, 2, 3, 4, 5}// 使用索引遍历
for i := 0; i < len(slice); i++ {fmt.Printf("索引%d: %d\n", i, slice[i])
}
  • 使用range遍历
slice := []string{"apple", "banana", "orange"}// 使用range遍历
for index, value := range slice {fmt.Printf("索引%d: %s\n", index, value)
}// 只获取值
for _, value := range slice {fmt.Printf("水果: %s\n", value)
}

2.3.5. 删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素。

  • 可以使用截取操作来删除前置元素和后置元素。
// 删除前置元素
slice := []int{1, 2, 3, 4, 5}
slice := slice[2:] // [3, 4, 5]// 删除后置元素
slice := []int{1, 2, 3, 4, 5}
slice := slice[:2] // [1,2]
  • 可以使用append内置函数+截取操作来删除元素
// 删除前置元素(不常用)
a = []int{1, 2, 3,4,5}
a = append(a[:0], a[2:]...) // [3, 4, 5]// 删除后置元素(不常用)
a = []int{1, 2, 3,4,5}
a = append(a[:0], a[:2]...) // [1,2]// 删除中间元素(常用)
a = []int{1, 2, 3,4,5}
a = append(a[:2], a[3:]...) // [1,2,4,5]
  • 可以使用copy内置函数+截取操作来删除元素
// 删除前置元素
a = []int{1, 2, 3,4,5}
a = a[:copy(a, a[2:])]// [3, 4, 5]// 删除后置元素
a = []int{1, 2, 3,4,5}
a = a[:copy(a, a[:2])] // [1,2]// 删除中间元素
a = []int{1, 2, 3,4,5}
a = a[:2+copy(a[2:], a[3:])] // [1,2,4,5]

最佳实践:

  • 使用截取操作来删除前置元素和后置元素。
  • 使用append内置函数+截取操作来删除中间元素

2.4. 切片的扩容机制

当切片容量不足时,append操作会触发扩容:

slice := make([]int, 0, 2)  // 长度0,容量2
slice = append(slice, 1)    // [1]
slice = append(slice, 2)    // [1 2]
slice = append(slice, 3)    // 触发扩容,容量变为4或更多

扩容规则:

  • 当所需容量超过当前容量时,会创建新的底层数组
  • 新容量通常是原容量的2倍(具体规则较为复杂)
  • 扩容后会将原数据复制到新数组

使用go1.25.0版本,从容量0开始填充元素,触发扩容的时机及结果如下:

func main() {slice := make([]int, 0, 0)fmt.Printf("len(%d) cap(%d)\n", len(slice), cap(slice))oldCap := cap(slice)for i := 0; i < 1000; i++ {slice = append(slice, i)if cap(slice) != oldCap {fmt.Printf("len(%d) cap(%d)\n", len(slice), cap(slice))oldCap = cap(slice)}}
}// len(0) cap(0)
// len(1) cap(4)
// len(5) cap(8)
// len(9) cap(16)
// len(17) cap(32)
// len(33) cap(64)
// len(65) cap(128)
// len(129) cap(256)
// len(257) cap(512)
// len(513) cap(848)
// len(849) cap(1280)

2.5. 切片的陷阱和最佳实践

2.5.1. 共享底层数组的问题

original := []int{1, 2, 3, 4, 5}
sub := original[1:3]  // [2 3]// 修改子切片会影响原切片
sub[0] = 20
fmt.Println(original)  // [1 20 3 4 5]

2.5.2. 避免切片污染

// 错误示例:可能导致意外的内存占用
func processLargeData() []byte {large := make([]byte, 1024*1024)  // 1MB数据// 处理数据...return large[100:200]  // 返回小切片,但仍引用整个1MB数组
}// 正确做法:复制数据
func processLargeDataCorrectly() []byte {large := make([]byte, 1024*1024)// 处理数据...result := make([]byte, 100)copy(result, large[100:200])return result
}

3. 映射(Map)

3.1. 映射的概念和特性

映射是一种无序的key-value键值对数据结构,类似于其他语言中的哈希表或字典。映射的特点包括:

  • 无序性:映射中的元素是无序的
  • 动态性:可以根据需要添加或删除键值对
  • 引用类型:映射是引用类型,零值为nil,必须初始化才能使用。
  • 键唯一性:每个键只能对应一个值

3.2 map的键类型限制

映射的键必须是可比较的类型,包括:

  • 布尔类型
  • 数字类型
  • 字符串类型
  • 指针类型
  • 通道类型
  • 接口类型
  • 结构体类型(所有字段都是可比较的)
  • 数组类型(元素类型是可比较的)

不可作为键的类型:

  • 切片
  • 映射
  • 函数

3.3. map的操作

3.3.1 map的声明和初始化

声明一个map的方式如下:

// 声明一个空映射(零值为nil)
var map1 map[string]int// 使用make创建映射
map2 := make(map[string]int)// 直接初始化
map3 := map[string]int{"apple":  5,"banana": 3,"orange": 8,
}// 声明并初始化
var map4 = map[string]string{"name": "张三","city": "北京",
}

map可以嵌套,如map[string]map[string]int

3.3.1. 添加和修改键值对

使用map[key]=value的方式,可以添加或者修改键值对。

  • 如果指定的key在map中不存在,则向map中添加键值对。
  • 如果指定的key在map中存在,则修改map中指定key对应的值。
// 创建映射
ages := make(map[string]int)// 添加元素
ages["张三"] = 25
ages["李四"] = 30// 修改元素
ages["张三"] = 26fmt.Println(ages)  // map[张三:26 李四:30]

3.3.2. 访问键值

使用v = map[key]的方式可以访问map中指定key的值。

  • 如果map中存在指定key,返回key对应的值。
  • 如果map中不存在指定的key,返回零值(值对应类型的零值)。
ages := map[string]int{"张三": 25,"李四": 30,
}// 获取存在的键
age := ages["张三"]
fmt.Println(age)  // 25// 获取不存在的键(返回零值)
age = ages["王五"]
fmt.Println(age)  // 0

使用v,ok = map[key]的方式,可以访问map中指定key的值,并且返回该值是否存在的标识。

  • 如果map中存在指定key,返回key对应的值,标识符为true。
  • 如果map中不存在指定的key,返回零值(值对应类型的零值),标识符为false。
ages := map[string]int{"张三": 25,"李四": 30,
}// 检查键是否存在
age, exists := ages["张三"]
if exists {fmt.Printf("张三的年龄是%d\n", age)
} else {fmt.Println("找不到张三的信息")
}// 简写形式
if age, ok := ages["李四"]; ok {fmt.Printf("李四的年龄是%d\n", age)
}

3.3.3. 删除元素

使用delete内置函数,可以删除map中指定的key。

删除一个不存在的key,并不会报错哦~

ages := map[string]int{"张三": 25,"李四": 30,"王五": 35,
}// 删除键值对
delete(ages, "王五")// 删除不存在的键不会报错
delete(ages, "赵六")fmt.Println(ages)  // map[张三:25 李四:30]

3.3.4. 遍历映射

遍历map有两种方式:

// 只遍历键
for:= range 映射名称{}
// 遍历键和值
for,:= range 映射名称{}

示例:

scores := map[string]int{"数学": 95,"语文": 87,"英语": 92,
}// 遍历映射(顺序不确定)
for subject, score := range scores {fmt.Printf("%s: %d分\n", subject, score)
}// 只获取键
for subject := range scores {fmt.Println("科目:", subject)
}// 只获取值
for _, score := range scores {fmt.Println("分数:", score)
}
http://www.dtcms.com/a/398093.html

相关文章:

  • 传统网站开发下载 wordpress语言包
  • flowable的监听器顺序
  • 连接局域网、主干网和虚拟局域网
  • 【保姆级】| 基于Docker的dify部署流程
  • 网站建设 策划方案如何用flashfxp通过ftp访问网站服务器下载网站代码
  • 大规模无人机检测数据集:11998张高质量图像,支持YOLOv8、COCO、TensorFlow多格式训练,涵盖飞机、无人机、直升机三大目标类别
  • 软考-系统规划与管理师教程(第2版)- 2025 新增 / 改版重点
  • sparkml 多列共享labelEncoder
  • 【TS5】Electron与Flutter
  • 线程池高频面试题(核心原理+配置实践+常见误区)
  • 【LeetCode热题100(28/100)】两数相加
  • 网站搭建思路如何使用模板建设网站
  • 注册网站的步骤二手房出售
  • 新疆燃气从业人员考试真题练习
  • 知识图谱的表示与推理对自然语言处理中因果性语义逻辑的影响与启示研究
  • go go-zero的学习,持续中...
  • C++篇 类和对象(3)万能工具怎么用?
  • 跨端边云时序数据管理新范式:Apache IoTDB 的 DB+AI 融合之道
  • 线程同步与互斥和生产消费模型
  • Java怎么终止一个线程
  • 软件项目管理中, UT测试如何体现
  • 神经网络工具箱
  • 软考系统架构设计师知识点-软件系统质量属性
  • 西安网站建设的软件哪个免费的网页制作软件最好
  • 【安装配置】【搭建本地Maven私服】
  • 一维卡尔曼滤波(无过程噪声)详解
  • AUTOSAR---汽车软件架构的标准化与未来展望
  • 压阻式应变传感器
  • Pydantic库应用
  • 【Linux手册】多线程编程的关键支撑:线程池与线程安全