《Go Cookbook CN》系列 13:数据结构实战——数组、切片与映射核心用法全解
本文聚焦Go语言三大核心基础数据结构(数组、切片、映射),从核心概念辨析到实际创建、访问、修改操作,再到并发安全、排序、映射操作等进阶场景,全方位拆解其底层特性与实战用法。学完本文,你将彻底掌握Go数组、切片、映射的核心使用方法,理解三者的本质区别与适用场景。
【本篇核心收获】
- 精准区分Go数组(值类型、固定长度)、切片(引用类型、基于数组)、映射(引用类型、哈希表)的核心特性与底层差异
- 掌握数组/切片的多种创建、访问、修改(添加/插入/删除)方法,理解切片长度与容量的核心区别
- 学会为数组/切片实现并发安全访问,规避goroutine数据竞争问题
- 掌握数组/切片的排序方法(基础类型排序、结构体切片排序),理解不同排序方式的性能差异
- 熟练掌握映射的创建、访问、修改、删除及排序操作,解决映射无序遍历的问题
1. Go基础数据结构核心认知
Go语言的四大基础数据结构包含数组、切片、映射和结构体(结构体单独讲解),其中数组、切片、映射是处理有序序列/键值对数据的核心,三者底层特性差异显著,是构建复杂数据结构的基础。
1.1 数组(Array)——固定长度的值类型有序序列
数组是表示相同类型元素的有序序列的数据结构,核心特性如下:
- 大小静态:定义时设定长度,后续无法更改
- 值类型:传递给函数时会拷贝整个数组,数据量大时存在性能消耗
- 零值填充:未显式初始化的数组,元素值为对应类型的零值
1.2 切片(Slice)——灵活的引用类型有序序列
切片是构建在数组之上的有序序列结构,比数组更常用,核心特性如下:
- 无固定长度:可动态扩展,灵活性远高于数组
- 引用类型:底层是一个结构体,包含「指向底层数组的指针、切片长度、底层数组容量」
- 低开销:传递时仅拷贝切片结构体(而非底层数组),处理大数据时效率更高

1.3 映射(Map)——键值对型引用类型结构
映射是将「键(key)」与「值(value)」关联的哈希表结构,核心特性如下:
- 引用类型:内部是指向
runtime.hmap结构体的指针 - 无序性:遍历映射时,键值对的顺序不固定
- 键唯一性:同一个映射中,键不能重复(重复赋值会覆盖原有值)
模块小结
本模块厘清了Go三大基础数据结构的核心特性——数组固定长度且为值类型,切片基于数组且为引用类型(含长度/容量双属性),映射为指向哈希表的引用类型,三者是构建复杂数据结构的基础。
2. 数组与切片的创建
数组和切片概念相似,但底层结构不同,创建方式既有共性也有差异,核心区别在于「长度是否固定」。
2.1 数组的定义与初始化
数组创建时必须指定长度,支持「仅声明」或「声明+初始化」两种方式,元素仅能为相同类型。
var numbers [10]int // 定义包含10个整数的数组,元素默认为零值
fmt.Println(numbers) // 输出:[0 0 0 0 0 0 0 0 0 0]
rhyme := [4]string{"twinkle", "twinkle", "little", "star"}
fmt.Println(rhyme) // 输出:[twinkle twinkle little star]注意事项:数组长度一旦确定无法修改,仅可修改元素值,这是数组灵活性低的核心原因。
2.2 切片的定义与初始化
切片无需指定长度,支持多种创建方式,核心是基于底层数组构建。
2.2.1 字面量定义
直接声明切片类型,可初始化也可空声明:
var integers []int // 空切片,输出:[]
var sheep = []string{"baa", "baa", "black", "sheep"}
fmt.Println(sheep) // 输出:[baa baa black sheep]2.2.2 make函数创建
指定元素类型、长度(必选)、容量(可选,默认等于长度):
// 创建长度10、容量10的整数切片,元素为零值
var integers = make([]int, 10)
fmt.Println(integers) // 输出:[0 0 0 0 0 0 0 0 0 0]
// 创建长度10、容量15的整数切片
integers := make([]int, 10, 15)
fmt.Println("length:", len(integers)) // 输出:length: 10
fmt.Println("capacity:", cap(integers)) // 输出:capacity: 152.2.3 new函数创建
返回指向切片/数组的指针,仅做「清零」不初始化:
// 创建指向切片的指针,输出:&[]
var ints *[]int = new([]int)
fmt.Println(ints)
// 创建指向数组的指针,输出:&[0 0 0 0 0 0 0 0 0 0]
var ints *[10]int = new([10]int)
fmt.Println(ints)核心区别:长度与容量
- 长度:切片中当前元素的数量(
len()获取) - 容量:底层数组的总长度(
cap()获取)
模块小结
本模块掌握了数组(指定长度、零值填充)与切片(无需指定长度、make/new创建)的多种创建方式,明确了切片长度(当前元素数)与容量(底层数组总长度)的核心区别。
3. 数组与切片的元素访问
数组和切片均为有序序列,访问方式完全一致,支持「索引访问」和「循环遍历」两类操作。
3.1 索引访问(单个/范围)
3.1.1 单个元素访问
通过「变量名[索引]」访问,索引从0开始:
numbers := []int{3, 14, 159, 26, 53, 59}
fmt.Println(numbers[3]) // 访问索引3的元素,输出:263.1.2 范围元素访问
通过「[起始索引:结束索引]」获取子序列(左闭右开),支持省略起始/结束索引:
numbers := []int{3, 14, 159, 26, 53, 59}
fmt.Println(numbers[2:4]) // 索引2到4(不含4),输出:[159 26]
fmt.Println(numbers[:4]) // 从0到4,输出:[3 14 159 26]
fmt.Println(numbers[2:]) // 从2到末尾,输出:[159 26 53 59]
// 数组转切片:通过范围访问将数组转换为切片
arr := [6]int{3, 14, 159, 26, 53, 59}
slice := arr[:] // 转换为切片3.2 循环遍历(for/for range)
3.2.1 普通for循环
通过遍历长度+索引访问元素:
numbers := []int{3, 14, 159, 26, 53, 59}
for i := 0; i < len(numbers); i++ {
fmt.Println(numbers[i])
}
// 输出:
// 3
// 14
// 159
// 26
// 53
// 593.2.2 for range循环
直接返回索引和元素值,更简洁:
numbers := []int{3, 14, 159, 26, 53, 59}
for i, v := range numbers {
fmt.Printf("i: %d, v: %d\n", i, v)
}
// 输出:
// i: 0, v: 3
// i: 1, v: 14
// i: 2, v: 159
// i: 3, v: 26
// i: 4, v: 53
// i: 5, v: 59模块小结
本模块掌握了数组/切片的两种核心访问方式——索引访问(支持单个元素/范围子序列)、循环遍历(普通for/for range),二者访问逻辑完全一致。
4. 数组与切片的修改操作
数组因长度固定仅支持「元素替换」,切片支持「替换、追加、插入、删除」等灵活操作。
4.1 元素替换
通过「索引赋值」修改元素值,数组和切片均支持:
numbers := []int{3, 14, 159, 26, 53, 58}
numbers[2] = 1000
fmt.Println(numbers) // 输出:[3 14 1000 26 53 58]4.2 切片元素添加(append)
append函数接收「切片+待追加元素」,返回新切片(需重新赋值):
numbers := []int{3, 14, 159, 26, 53, 58}
// 追加单个元素
numbers = append(numbers, 97)
fmt.Println(numbers) // 输出:[3 14 159 26 53 58 97]
// 追加多个元素
numbers = append(numbers, 97, 932, 38, 4, 626)
// 追加另一个切片(需解包...)
nums := []int{97, 932, 38, 4, 626}
numbers = append(numbers, nums...)注意事项:不能同时追加「单个元素+切片解包」(如append(numbers, 1, nums...)会报错)。
4.3 切片元素插入
Go无直接插入函数,需通过append模拟,核心是「拆分切片+拼接」:
4.3.1 中间插入单个元素
numbers := []int{3, 14, 159, 26, 53, 58}
// 在索引2后插入1000
numbers = append(numbers[:3], numbers[2:]...) // 预留位置
numbers[2] = 1000
fmt.Println(numbers) // 输出:[3 14 159 1000 26 53 58]4.3.2 开头插入元素
numbers := []int{3, 14, 159, 26, 53, 58}
numbers = append([]int{2000}, numbers...)
fmt.Println(numbers) // 输出:[2000 3 14 159 26 53 58]4.3.3 插入另一个切片
numbers := []int{3, 14, 159, 26, 53, 58}
inserted := []int{1000, 2000, 3000, 4000}
// 保存原始切片尾部(避免浅拷贝导致数据覆盖)
tail := append([]int{}, numbers[3:]...)
// 拼接前部+插入切片+尾部
numbers = append(numbers[:3], inserted...)
numbers = append(numbers, tail...)
fmt.Println(numbers) // 输出:[3 14 159 1000 2000 3000 4000 26 53 58]4.4 切片元素删除
通过「拼接删除位置前后的切片」实现,核心是append:
4.4.1 删除开头元素
numbers := []int{3, 14, 159, 26, 53, 58}
numbers = numbers[1:] // 移除索引0的元素,输出:[14 159 26 53 58]4.4.2 删除末尾元素
numbers := []int{3, 14, 159, 26, 53, 58}
numbers = numbers[:len(numbers)-1] // 移除最后一个元素,输出:[3 14 159 26 53]4.4.3 删除中间元素
numbers := []int{3, 14, 159, 26, 53, 58}
// 移除索引2的元素(159)
numbers = append(numbers[:2], numbers[3:]...)
fmt.Println(numbers) // 输出:[3 14 26 53 58]避坑指南
- 数组长度固定,无法添加/插入/删除元素,仅可修改元素值
- 切片插入/删除操作本质是重新构建切片,需注意底层数组的引用特性(浅拷贝可能导致数据意外修改)
模块小结
本模块掌握了数组(仅支持元素替换)与切片(支持追加/插入/删除)的修改操作,核心是利用append函数实现切片的灵活修改,需注意数组的长度限制。
5. 数组/切片的并发安全处理
数组和切片本身不支持并发安全访问,多goroutine操作共享数组/切片会引发数据竞争,需通过sync.Mutex互斥锁解决。
5.1 数据竞争问题复现
多goroutine同时修改共享切片,导致数据混乱:
import (
"fmt"
"time"
)
var shared []int = []int{1, 2, 3, 4, 5, 6}
// 元素加1
func increase(num int) {
fmt.Printf("[%d a] : %v\n", num, shared)
for i := 0; i < len(shared); i++ {
time.Sleep(20 * time.Microsecond)
shared[i] = shared[i] + 1
}
fmt.Printf("[%d b] : %v\n", num, shared)
}
// 元素减1
func decrease(num int) {
fmt.Printf("[%d a] : %v\n", num, shared)
for i := 0; i < len(shared); i++ {
time.Sleep(10 * time.Microsecond)
shared[i] = shared[i] - 1
}
fmt.Printf("[%d b] : %v\n", num, shared)
}
func main() {
for i := 0; i < 5; i++ {
go increase(i)
}
for i := 0; i < 5; i++ {
go decrease(i)
}
time.Sleep(2 * time.Second)
}问题现象:goroutine执行顺序随机,共享切片的值被无序修改,结果不符合预期。
5.2 互斥锁(sync.Mutex)解决并发安全
通过「加锁→修改→解锁」保证同一时间仅一个goroutine访问共享资源:
import (
"fmt"
"sync"
"time"
)
var shared []int = []int{1, 2, 3, 4, 5, 6}
var mutex sync.Mutex
// 加锁实现元素加1
func increaseWithMutex(num int) {
mutex.Lock() // 修改前加锁
fmt.Printf("[+%d a] : %v\n", num, shared)
for i := 0; i < len(shared); i++ {
time.Sleep(20 * time.Microsecond)
shared[i] = shared[i] + 1
}
fmt.Printf("[+%d b] : %v\n", num, shared)
mutex.Unlock() // 修改后解锁
}
// 加锁实现元素减1
func decreaseWithMutex(num int) {
mutex.Lock() // 修改前加锁
fmt.Printf("[-%d a] : %v\n", num, shared)
for i := 0; i < len(shared); i++ {
time.Sleep(10 * time.Microsecond)
shared[i] = shared[i] - 1
}
fmt.Printf("[-%d b] : %v\n", num, shared)
mutex.Unlock() // 修改后解锁
}
func main() {
for i := 0; i < 5; i++ {
go increaseWithMutex(i)
}
for i := 0; i < 5; i++ {
go decreaseWithMutex(i)
}
time.Sleep(2 * time.Second)
}效果:goroutine串行执行,共享切片的修改有序,结果符合预期。
避坑指南
- 未加锁的共享数组/切片在多goroutine中会出现数据竞争,必须通过
sync.Mutex保证同一时间仅一个goroutine访问 - 加锁范围需覆盖「所有修改共享资源的逻辑」,避免部分操作未加锁导致的竞争
模块小结
本模块通过sync.Mutex互斥锁解决了数组/切片的并发访问安全问题,核心是修改共享资源前加锁、修改后解锁,规避数据竞争。
6. 数组/切片的排序操作
Go的sort包提供了丰富的排序方法,支持基础类型切片、结构体切片的排序,可灵活实现升序/降序。
6.1 基础类型切片排序(int/float64/string)
sort包内置函数直接排序,默认升序:
import "sort"
func main() {
integers := []int{3, 14, 159, 26, 53}
floats := []float64{3.14, 1.41, 1.73, 2.72, 4.53}
strings := []string{"the", "quick", "brown", "fox", "jumped"}
// 升序排序
sort.Ints(integers)
sort.Float64s(floats)
sort.Strings(strings)
fmt.Println(integers) // 输出:[3 14 26 53 159]
fmt.Println(floats) // 输出:[1.41 1.73 2.72 3.14 4.53]
fmt.Println(strings) // 输出:[brown fox jumped quick the]
// 降序排序:反转已排序的切片
for i := len(integers)/2 - 1; i >= 0; i-- {
opp := len(integers) - 1 - i
integers[i], integers[opp] = integers[opp], integers[i]
}
fmt.Println(integers) // 输出:[159 53 26 14 3]
// 降序排序:自定义less函数
sort.Slice(floats, func(i, j int) bool {
return floats[i] > floats[j]
})
fmt.Println(floats) // 输出:[4.53 3.14 2.72 1.73 1.41]
}6.2 结构体切片排序
6.2.1 sort.Slice(简单灵活)
通过自定义比较函数排序,支持任意结构体字段:
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{"Alice", 22},
{"Bob", 18},
{"Charlie", 23},
{"Dave", 27},
{"Eve", 31},
}
// 按年龄升序排序
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println(people) // 输出:[{Bob 18} {Alice 22} {Charlie 23} {Dave 27} {Eve 31}]
}注意:sort.Slice可能打乱相等元素的原始顺序,需保持顺序可使用sort.SliceStable。
6.2.2 实现sort.Interface(性能更优)
为切片类型实现Len()、Less()、Swap()方法,支持更多高级操作:
import "sort"
type Person struct {
Name string
Age int
}
// 定义切片类型别名
type ByAge []Person
// 实现sort.Interface接口
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
people := []Person{
{"Alice", 22},
{"Bob", 18},
{"Charlie", 23},
{"Dave", 27},
{"Eve", 31},
}
// 升序排序
sort.Sort(ByAge(people))
fmt.Println(people) // 输出:[{Bob 18} {Alice 22} {Charlie 23} {Dave 27} {Eve 31}]
// 降序排序
sort.Sort(sort.Reverse(ByAge(people)))
fmt.Println(people) // 输出:[{Eve 31} {Dave 27} {Charlie 23} {Alice 22} {Bob 18}]
// 检查是否已排序
fmt.Println(sort.IsSorted(ByAge(people))) // 输出:true
}性能对比
| 排序方式 | 性能(示例基准测试) | 优势 |
|---|---|---|
| sort.Slice | 108.9 ns/op | 简单、无需定义额外类型 |
| sort.Interface | 44.33 ns/op | 性能更高、支持降序/校验 |
避坑指南
sort.Slice可能打乱相等元素的原始顺序,需保持顺序可使用sort.SliceStable- 实现
sort.Interface接口虽然繁琐,但性能优于sort.Slice,适合高频排序场景
模块小结
本模块掌握了数组/切片的排序方法——基础类型用sort包内置函数,结构体切片可用sort.Slice或实现sort.Interface接口(性能更优),支持升序/降序排序。
7. 映射(Map)的核心操作
映射是Go中处理键值对数据的核心结构,支持创建、访问、修改、删除、有序遍历等操作。
7.1 映射的创建与初始化
映射使用前必须初始化(零值为nil,无法直接使用),支持两种创建方式:
// 方式1:声明+make初始化
var people map[string]int // 声明(未初始化,nil)
people = make(map[string]int) // 初始化
people["Alice"] = 22 // 添加键值对
// 方式2:声明+初始化一步完成
people := make(map[string]int)
// 方式3:字面量初始化(直接添加键值对)
people := map[string]int{
"Alice": 22,
"Bob": 18,
"Charlie": 23,
"Dave": 27,
"Eve": 31,
}
fmt.Println(people) // 输出:map[Alice:22 Bob:18 Charlie:23 Dave:27 Eve:31]7.2 映射的元素访问
7.2.1 键访问
通过「映射名[键]」访问值,键不存在时返回值类型零值:
people := map[string]int{"Alice":22, "Bob":18}
fmt.Println(people["Alice"]) // 存在,输出:22
fmt.Println(people["Nemo"]) // 不存在,输出:0(int零值)7.2.2 comma ok模式(判断键是否存在)
解决「零值 vs 键不存在」的区分问题:
people := map[string]int{"Alice":22, "Bob":18}
age, ok := people["Nemo"]
if ok {
fmt.Println("Key exists with value:", age)
} else {
fmt.Println("Key does not exist") // 输出:Key does not exist
}7.2.3 遍历操作
通过for range遍历,返回键和值(顺序不固定):
people := map[string]int{
"Alice": 22,
"Bob": 18,
"Charlie": 23,
}
// 遍历键值对
for k, v := range people {
fmt.Println(k, v)
}
// 仅遍历键
for k := range people {
fmt.Println(k)
}
// 仅遍历值(需手动收集)
var values []int
for _, v := range people {
values = append(values, v)
}
fmt.Println(values) // 输出:[22 18 23](顺序不固定)7.3 映射的修改与删除
7.3.1 修改值
直接为已有键赋值,覆盖原有值:
people := map[string]int{"Alice":22}
people["Alice"] = 23 // 修改值
fmt.Println(people["Alice"]) // 输出:237.3.2 删除键值对
使用delete函数,删除不存在的键不会报错:
people := map[string]int{"Alice":22, "Bob":18}
delete(people, "Alice") // 删除键Alice
fmt.Println(people) // 输出:map[Bob:18]
delete(people, "Nemo") // 删除不存在的键,无报错7.4 映射的有序遍历(按键排序)
映射本身无序,需通过「提取键→排序键→按键遍历」实现有序:
people := map[string]int{
"Alice": 22,
"Bob": 18,
"Charlie": 23,
"Dave": 27,
"Eve": 31,
}
// 步骤1:提取所有键到切片
var keys []string
for k := range people {
keys = append(keys, k)
}
// 步骤2:排序键切片
sort.Strings(keys)
// 步骤3:按排序后的键遍历映射
for _, key := range keys {
fmt.Println(key, people[key])
}
// 输出(有序):
// Alice 22
// Bob 18
// Charlie 23
// Dave 27
// Eve 31避坑指南
- 访问不存在的映射键会返回值类型零值,需用comma ok模式判断键是否存在
- 映射本身无序,有序遍历需先提取键到切片并排序,再按排序后的键遍历
模块小结
本模块掌握了映射的创建、访问、修改、删除及有序遍历操作,核心是利用comma ok模式判断键存在性,通过键切片排序实现有序遍历。
【本篇核心知识点速记】
- 核心特性:数组(值类型、固定长度)、切片(引用类型、长度/容量、基于数组)、映射(引用类型、键值对、底层hmap指针)
- 数组/切片创建:数组必须指定长度,切片可通过字面量/make/new创建,make可指定长度/容量
- 数组/切片操作:支持索引访问/循环遍历,切片可通过append实现追加/插入/删除,数组仅可修改元素
- 并发安全:数组/切片需通过sync.Mutex加锁保证多goroutine安全访问
- 排序:基础类型用sort内置函数,结构体切片用sort.Slice或实现sort.Interface(性能更优)
- 映射操作:创建需初始化,访问用comma ok模式,删除用delete函数,有序遍历需先排序键切片
