Skip to content

《Go Cookbook CN》系列 13:数据结构实战——数组、切片与映射核心用法全解

约 4725 字大约 16 分钟

《Go Cookbook CN》系列Go语言

2026-04-02

本文聚焦Go语言三大核心基础数据结构(数组、切片、映射),从核心概念辨析到实际创建、访问、修改操作,再到并发安全、排序、映射操作等进阶场景,全方位拆解其底层特性与实战用法。学完本文,你将彻底掌握Go数组、切片、映射的核心使用方法,理解三者的本质区别与适用场景。

【本篇核心收获】

  • 精准区分Go数组(值类型、固定长度)、切片(引用类型、基于数组)、映射(引用类型、哈希表)的核心特性与底层差异
  • 掌握数组/切片的多种创建、访问、修改(添加/插入/删除)方法,理解切片长度与容量的核心区别
  • 学会为数组/切片实现并发安全访问,规避goroutine数据竞争问题
  • 掌握数组/切片的排序方法(基础类型排序、结构体切片排序),理解不同排序方式的性能差异
  • 熟练掌握映射的创建、访问、修改、删除及排序操作,解决映射无序遍历的问题

1. Go基础数据结构核心认知

Go语言的四大基础数据结构包含数组、切片、映射和结构体(结构体单独讲解),其中数组、切片、映射是处理有序序列/键值对数据的核心,三者底层特性差异显著,是构建复杂数据结构的基础。

1.1 数组(Array)——固定长度的值类型有序序列

数组是表示相同类型元素的有序序列的数据结构,核心特性如下:

  • 大小静态:定义时设定长度,后续无法更改
  • 值类型:传递给函数时会拷贝整个数组,数据量大时存在性能消耗
  • 零值填充:未显式初始化的数组,元素值为对应类型的零值

1.2 切片(Slice)——灵活的引用类型有序序列

切片是构建在数组之上的有序序列结构,比数组更常用,核心特性如下:

  • 无固定长度:可动态扩展,灵活性远高于数组
  • 引用类型:底层是一个结构体,包含「指向底层数组的指针、切片长度、底层数组容量」
  • 低开销:传递时仅拷贝切片结构体(而非底层数组),处理大数据时效率更高

图1:切片与底层数组的关系

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: 15

2.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的元素,输出:26

3.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
// 59

3.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.Slice108.9 ns/op简单、无需定义额外类型
sort.Interface44.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"]) // 输出:23

7.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模式判断键存在性,通过键切片排序实现有序遍历。

【本篇核心知识点速记】

  1. 核心特性:数组(值类型、固定长度)、切片(引用类型、长度/容量、基于数组)、映射(引用类型、键值对、底层hmap指针)
  2. 数组/切片创建:数组必须指定长度,切片可通过字面量/make/new创建,make可指定长度/容量
  3. 数组/切片操作:支持索引访问/循环遍历,切片可通过append实现追加/插入/删除,数组仅可修改元素
  4. 并发安全:数组/切片需通过sync.Mutex加锁保证多goroutine安全访问
  5. 排序:基础类型用sort内置函数,结构体切片用sort.Slice或实现sort.Interface(性能更优)
  6. 映射操作:创建需初始化,访问用comma ok模式,删除用delete函数,有序遍历需先排序键切片