24 KiB
24 KiB
第三章:数据结构详解 —— 数组、切片、映射与结构体
本章目标:深入理解 Go 的四种核心数据结构,掌握数组与切片的本质区别、映射的底层实现、结构体的组合与嵌入,以及在实际开发中的最佳实践。
3.1 数组(Arrays):固定长度的序列
3.1.1 数组的基本概念
数组是固定长度的相同类型元素的集合。一旦声明,长度不可改变。
package main
import "fmt"
func main() {
// 声明数组(长度是类型的一部分)
var arr1 [5]int // 5 个 int,初始化为 0
var arr2 [3]string = {"A", "B", "C"}
// 声明并初始化
arr3 := [4]int{10, 20, 30, 40}
// 部分初始化,剩余为 0
arr4 := [5]int{1, 2} // [1 2 0 0 0]
// 使用 ... 自动推断长度
arr5 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]
fmt.Printf("arr1: %v\n", arr1)
fmt.Printf("arr2: %v\n", arr2)
fmt.Printf("arr3: %v\n", arr3)
fmt.Printf("arr4: %v\n", arr4)
fmt.Printf("arr5: %v\n", arr5)
}
深度解析:
- 数组长度是类型的一部分:
[5]int和[10]int是不同类型 - 数组是值类型:赋值或传参时会完整复制
- 数组长度不可变,这是与切片的核心区别
3.1.2 数组的访问与遍历
func arrayAccess() {
arr := [5]int{10, 20, 30, 40, 50}
// 访问元素
fmt.Println(arr[0]) // 10
fmt.Println(arr[4]) // 50
// fmt.Println(arr[5]) // panic: index out of range
// 修改元素
arr[1] = 99
fmt.Println(arr) // [10 99 30 40 50]
// 获取长度
fmt.Println(len(arr)) // 5
// 遍历
for i, v := range arr {
fmt.Printf("索引 %d: 值 %d\n", i, v)
}
// 只遍历值
for _, v := range arr {
fmt.Println(v)
}
}
3.1.3 多维数组
func multiDimensionalArray() {
// 2x3 的二维数组
var matrix [2][3]int
// 初始化
matrix = [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
// 或者
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
// 访问
fmt.Println(matrix[0][1]) // 2
fmt.Println(matrix[1][2]) // 6
// 遍历
for i, row := range matrix {
for j, val := range row {
fmt.Printf("matrix[%d][%d] = %d\n", i, j, val)
}
}
}
3.1.4 数组的局限性
问题 1:值复制性能问题
func arrayCopyDemo() {
arr := [10000]int{}
// 初始化...
// 传参时会复制整个数组(性能差)
processArray(arr)
}
func processArray(a [10000]int) {
// 这里接收的是副本
}
// 正确做法:传指针
func processArrayPtr(a *[10000]int) {
// 只传递地址
}
问题 2:长度固定
arr := [5]int{1, 2, 3, 4, 5}
// arr[5] = 6 // 编译错误!数组长度固定
结论:在实际开发中,极少直接使用数组,更多使用切片(Slice)。
3.2 切片(Slices):动态长度的视图
切片是 Go 中最常用的数据结构,它是对数组的动态视图,提供了灵活的长度和容量管理。
3.2.1 切片的底层结构
切片不是数组,它是一个描述符,包含三个字段:
ptr:指向底层数组的指针len:切片长度cap:切片容量(从 ptr 开始到数组末尾的长度)
type SliceHeader struct {
Data uintptr // 指向底层数组
Len int // 长度
Cap int // 容量
}
3.2.2 切片的创建
方式 1:从数组创建
func sliceFromArray() {
arr := [5]int{1, 2, 3, 4, 5}
// 切片语法:arr[start:end]
s1 := arr[0:3] // [1 2 3], len=3, cap=5
s2 := arr[1:] // [2 3 4 5], len=4, cap=4
s3 := arr[:3] // [1 2 3], len=3, cap=5
s4 := arr[:] // [1 2 3 4 5], len=5, cap=5
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
fmt.Printf("s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("s4: %v, len=%d, cap=%d\n", s4, len(s4), cap(s4))
// 修改切片会影响原数组
s1[0] = 99
fmt.Println(arr) // [99 2 3 4 5]
}
方式 2:make 创建
func sliceMake() {
// make([]T, length, capacity)
s1 := make([]int, 5) // len=5, cap=5, 元素为 0
s2 := make([]int, 3, 10) // len=3, cap=10, 元素为 0
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
// 直接初始化
s3 := []int{1, 2, 3, 4, 5} // len=5, cap=5
s4 := []string{"a", "b", "c"} // len=3, cap=3
}
方式 3:字面量创建
func sliceLiteral() {
s := []int{1, 2, 3, 4, 5}
fmt.Printf("s: %v, len=%d, cap=%d\n", s, len(s), cap(s))
}
3.2.3 切片的操作
append:追加元素
func sliceAppend() {
s := []int{1, 2, 3}
// 追加单个元素
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
// 追加多个元素
s = append(s, 5, 6, 7)
fmt.Println(s) // [1 2 3 4 5 6 7]
// 追加另一个切片
s2 := []int{8, 9, 10}
s = append(s, s2...) // 必须加 ... 展开
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10]
// 容量变化
s3 := make([]int, 0, 5)
fmt.Printf("初始:len=%d, cap=%d\n", len(s3), cap(s3))
for i := 1; i <= 10; i++ {
s3 = append(s3, i)
fmt.Printf("追加 %d: len=%d, cap=%d\n", i, len(s3), cap(s3))
}
}
深度解析:
append会返回新切片,必须接收返回值- 当
len < cap时,直接在底层数组追加 - 当
len == cap时,会重新分配更大的数组并复制 - 扩容策略:
- 容量 < 1024:翻倍
- 容量 >= 1024:增长约 25%
copy:复制切片
func sliceCopy() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // 复制 min(len(dst), len(src)) 个元素
fmt.Printf("复制了 %d 个元素\n", n)
fmt.Printf("dst: %v\n", dst) // [1 2 3]
// 从指定位置复制
dst2 := make([]int, 5)
copy(dst2[1:], src)
fmt.Printf("dst2: %v\n", dst2) // [0 1 2 3 4]
}
切片切片(Slicing the slice)
func sliceSlicing() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s1 := s[2:5] // [3 4 5], len=3, cap=8
s2 := s1[1:3] // [4 5], len=2, cap=6(从 s1[1] 开始,到原数组末尾)
fmt.Printf("s: %v\n", s)
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
// 修改 s2 会影响 s
s2[0] = 99
fmt.Printf("s 修改后:%v\n", s) // [1 2 3 99 5 6 7 8 9 10]
}
深度解析:
- 切片的容量是从当前起始位置到原数组末尾
- 多次切片后,容量可能很大,导致内存无法释放
- 最佳实践:如果不再需要原数组,使用
append创建新切片
// 释放内存的正确方式
func trimMemory(s []int) []int {
result := append([]int(nil), s[:3]...) // 创建新切片,只包含前 3 个元素
return result
}
3.2.4 切片的常见陷阱
陷阱 1:共享底层数组
func sharedUnderlyingArray() {
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3]
s2[0] = 99
fmt.Println(s1) // [1 99 3 4 5] // s1 也被修改了!
fmt.Println(s2) // [99 3]
}
陷阱 2:容量陷阱导致内存泄漏
func capacityLeak() {
data := make([]int, 1000000) // 大数组
// 处理数据...
// 错误:只取前 10 个,但容量仍为 1000000
small := data[:10]
// small 持有整个大数组的引用,内存无法释放
// 正确:创建新切片
small := append([]int(nil), data[:10]...)
// 现在 data 可以被垃圾回收
}
陷阱 3:append 后的长度变化
func appendLengthChange() {
s := make([]int, 3)
s[0], s[1], s[2] = 1, 2, 3
s = append(s, 4, 5, 6)
// s 现在是 [1 2 3 4 5 6], len=6
// 如果继续用原来的索引访问会 panic
// s[3] = 7 // 正确
// s[10] = 8 // panic: index out of range
}
3.2.5 切片的高级用法
使用切片作为函数参数
func processSlice(s []int) {
// 修改会影响原切片(因为共享底层数组)
for i := range s {
s[i] *= 2
}
}
func main() {
s := []int{1, 2, 3}
processSlice(s)
fmt.Println(s) // [2 4 6]
}
切片推导(Slice Comprehension)
Go 没有像 Python 那样的列表推导式,但可以用循环实现:
func sliceComprehension() {
nums := []int{1, 2, 3, 4, 5}
// 平方
squares := make([]int, 0, len(nums))
for _, n := range nums {
squares = append(squares, n*n)
}
fmt.Println(squares) // [1 4 9 16 25]
// 过滤偶数
evens := make([]int, 0)
for _, n := range nums {
if n%2 == 0 {
evens = append(evens, n)
}
}
fmt.Println(evens) // [2 4]
}
3.3 映射(Maps):键值对的集合
映射是 Go 中的哈希表实现,提供 O(1) 的平均查找时间。
3.3.1 映射的创建与初始化
func mapCreation() {
// 方式 1:make 创建
m1 := make(map[string]int)
m1["one"] = 1
m1["two"] = 2
// 方式 2:字面量初始化
m2 := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
// 方式 3:空映射
m3 := map[string]int{}
// nil 映射(不能写入)
var m4 map[string]int
// m4["five"] = 5 // panic: assignment to entry in nil map
fmt.Printf("m1: %v\n", m1)
fmt.Printf("m2: %v\n", m2)
fmt.Printf("m3: %v\n", m3)
}
3.3.2 映射的基本操作
func mapOperations() {
m := map[string]int{
"apple": 5,
"banana": 3,
"orange": 8,
}
// 读取
fmt.Println(m["apple"]) // 5
// 写入
m["grape"] = 10
m["apple"] = 7 // 更新
// 删除
delete(m, "banana")
// 检查键是否存在(重要!)
value, exists := m["orange"]
if exists {
fmt.Printf("orange 存在,价格:%d\n", value)
}
// 读取不存在的键,返回零值
fmt.Println(m["mango"]) // 0(int 的零值)
// 遍历(无序!)
for fruit, price := range m {
fmt.Printf("%s: %d\n", fruit, price)
}
// 只遍历键
for fruit := range m {
fmt.Println(fruit)
}
// 只遍历值
for _, price := range m {
fmt.Println(price)
}
}
3.3.3 映射的底层原理
深度解析:
- 映射是引用类型,底层指向一个
hmap结构 - 映射是无序的,每次遍历顺序可能不同(故意设计,防止依赖顺序)
- 映射不是并发安全的,多线程读写会 panic
- 映射的容量会自动增长,但不会自动缩小
// 并发安全的映射
func concurrentMap() {
var m sync.Map // 或使用 sync.RWMutex 保护普通 map
m.Store("key", "value")
val, _ := m.Load("key")
fmt.Println(val)
}
3.3.4 映射的常见陷阱
陷阱 1:遍历无序
func mapUnordered() {
m := map[int]string{
1: "one",
2: "two",
3: "three",
}
for i := 0; i < 5; i++ {
for k, v := range m {
fmt.Printf("%d:%s ", k, v)
}
fmt.Println()
}
// 每次输出顺序都不同!
}
陷阱 2:nil 映射
func mapNil() {
var m map[string]int // nil 映射
// 读取没问题(返回零值)
fmt.Println(m["key"]) // 0
// 写入会 panic
// m["key"] = 1 // panic: assignment to entry in nil map
// 正确做法
m = make(map[string]int)
m["key"] = 1
}
陷阱 3:映射作为函数参数
func modifyMap(m map[string]int) {
m["new"] = 100 // 修改会影响原映射
}
func main() {
m := make(map[string]int)
modifyMap(m)
fmt.Println(m) // map[new:100]
}
3.3.5 高级用法:嵌套映射
func nestedMap() {
// 映射的映射
scores := map[string]map[string]int{
"Alice": {
"math": 95,
"english": 88,
},
"Bob": {
"math": 92,
"english": 90,
},
}
// 访问
fmt.Println(scores["Alice"]["math"]) // 95
// 动态创建
if scores["Charlie"] == nil {
scores["Charlie"] = make(map[string]int)
}
scores["Charlie"]["math"] = 85
// 遍历
for name, subjects := range scores {
fmt.Printf("%s:\n", name)
for subject, score := range subjects {
fmt.Printf(" %s: %d\n", subject, score)
}
}
}
3.4 结构体(Structs):自定义类型
结构体是 Go 中组合数据的方式,类似其他语言的类(但没有继承)。
3.4.1 结构体的定义与初始化
func structDefinition() {
// 定义结构体
type Person struct {
Name string
Age int
Email string
}
// 方式 1:字面量初始化(推荐)
p1 := Person{
Name: "Alice",
Age: 25,
Email: "alice@example.com",
}
// 方式 2:位置初始化(不推荐,易错)
p2 := Person{"Bob", 30, "bob@example.com"}
// 方式 3:部分初始化
p3 := Person{Name: "Charlie"} // Age=0, Email=""
// 方式 4:new(返回指针)
p4 := new(Person)
p4.Name = "David"
p4.Age = 28
// 方式 5:取地址
p5 := &Person{Name: "Eve", Age: 22}
fmt.Printf("p1: %+v\n", p1)
fmt.Printf("p2: %+v\n", p2)
fmt.Printf("p3: %+v\n", p3)
fmt.Printf("p4: %+v\n", *p4)
fmt.Printf("p5: %+v\n", *p5)
}
3.4.2 结构体字段访问
func structAccess() {
type Point struct {
X float64
Y float64
}
p := Point{X: 10.5, Y: 20.3}
// 值访问
fmt.Println(p.X) // 10.5
p.X = 15.0
// 指针访问(自动解引用)
ptr := &p
fmt.Println(ptr.X) // 15.0(不需要 ptr->X)
ptr.Y = 25.0
fmt.Printf("p: %+v\n", p) // {X:15 Y:25}
}
3.4.3 结构体方法
func structMethods() {
type Rectangle struct {
Width float64
Height float64
}
// 值接收者方法
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 指针接收者方法(可以修改结构体)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
rect := Rectangle{Width: 10, Height: 5}
fmt.Printf("面积:%.2f\n", rect.Area()) // 50.00
rect.Scale(2.0)
fmt.Printf("缩放后面积:%.2f\n", rect.Area()) // 200.00
}
深度解析:
- 值接收者:方法接收结构体的副本,不能修改原结构体
- 指针接收者:方法接收结构体的指针,可以修改原结构体
- 如果方法需要修改接收者,必须使用指针接收者
- 如果方法只读取,可以使用值接收者(小结构体)或指针接收者(大结构体,避免复制)
3.4.4 结构体嵌入(组合)
Go 没有继承,但通过嵌入实现类似功能。
func structEmbedding() {
// 基础结构体
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Printf("你好,我是 %s\n", p.Name)
}
// 嵌入结构体
type Employee struct {
Person // 匿名嵌入
ID int
Salary float64
}
e := Employee{
Person: Person{Name: "Alice", Age: 30},
ID: 1001,
Salary: 50000,
}
// 直接访问嵌入字段
fmt.Println(e.Name) // "Alice"(等价于 e.Person.Name)
fmt.Println(e.Age) // 30
// 调用嵌入方法
e.SayHello() // "你好,我是 Alice"
// 方法重写
type Manager struct {
Employee
TeamSize int
}
// Manager 也继承了 SayHello 方法
m := Manager{
Employee: Employee{
Person: Person{Name: "Bob", Age: 40},
ID: 2001,
Salary: 80000,
},
TeamSize: 10,
}
m.SayHello() // "你好,我是 Bob"
}
3.4.5 结构体标签(Tags)
结构体标签用于元数据,常用于 JSON、数据库映射等。
func structTags() {
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" validate:"required"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // 忽略此字段
Age int `json:"age" validate:"min=18"`
}
u := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
Password: "secret123",
Age: 25,
}
// JSON 序列化
jsonData, _ := json.Marshal(u)
fmt.Println(string(jsonData))
// 输出:{"id":1,"name":"Alice","email":"alice@example.com","age":25}
// Password 被忽略(json:"-")
// Email 即使为空也会输出(没有 omitempty 时)
}
3.4.6 结构体比较
func structComparison() {
type Point struct {
X int
Y int
}
p1 := Point{X: 1, Y: 2}
p2 := Point{X: 1, Y: 2}
p3 := Point{X: 1, Y: 3}
fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false
// 包含不可比较字段的结构体不能比较
type Data struct {
Name string
List []int // 切片不可比较
}
// d1 := Data{Name: "a", List: []int{1}}
// d2 := Data{Name: "a", List: []int{1}}
// fmt.Println(d1 == d2) // 编译错误!
}
3.5 深度实践:综合案例
3.5.1 学生管理系统
package main
import (
"encoding/json"
"fmt"
"sort"
)
// 学生结构体
type Student struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Score float64 `json:"score"`
}
// 学生管理系统
type StudentManager struct {
students map[int]*Student
nextID int
}
// 创建管理系统
func NewStudentManager() *StudentManager {
return &StudentManager{
students: make(map[int]*Student),
nextID: 1,
}
}
// 添加学生
func (sm *StudentManager) AddStudent(name string, age int, score float64) int {
id := sm.nextID
sm.nextID++
sm.students[id] = &Student{
ID: id,
Name: name,
Age: age,
Score: score,
}
return id
}
// 获取学生
func (sm *StudentManager) GetStudent(id int) *Student {
return sm.students[id]
}
// 更新学生
func (sm *StudentManager) UpdateStudent(id int, name string, age int, score float64) bool {
if student, exists := sm.students[id]; exists {
student.Name = name
student.Age = age
student.Score = score
return true
}
return false
}
// 删除学生
func (sm *StudentManager) DeleteStudent(id int) bool {
if _, exists := sm.students[id]; exists {
delete(sm.students, id)
return true
}
return false
}
// 获取所有学生
func (sm *StudentManager) GetAllStudents() []*Student {
students := make([]*Student, 0, len(sm.students))
for _, s := range sm.students {
students = append(students, s)
}
return students
}
// 按分数排序
func (sm *StudentManager) SortByScore(descending bool) []*Student {
students := sm.GetAllStudents()
sort.Slice(students, func(i, j int) bool {
if descending {
return students[i].Score > students[j].Score
}
return students[i].Score < students[j].Score
})
return students
}
// 导出为 JSON
func (sm *StudentManager) ExportJSON() (string, error) {
data, err := json.MarshalIndent(sm.students, "", " ")
if err != nil {
return "", err
}
return string(data), nil
}
func main() {
manager := NewStudentManager()
// 添加学生
id1 := manager.AddStudent("Alice", 20, 95.5)
id2 := manager.AddStudent("Bob", 22, 88.0)
id3 := manager.AddStudent("Charlie", 21, 92.5)
fmt.Printf("添加学生 ID: %d, %d, %d\n", id1, id2, id3)
// 获取学生
student := manager.GetStudent(id1)
fmt.Printf("学生: %+v\n", student)
// 更新学生
manager.UpdateStudent(id1, "Alice", 21, 97.0)
fmt.Printf("更新后: %+v\n", manager.GetStudent(id1))
// 删除学生
manager.DeleteStudent(id2)
fmt.Printf("删除后学生数量:%d\n", len(manager.GetAllStudents()))
// 排序
sorted := manager.SortByScore(true)
fmt.Println("\n按分数降序排列:")
for _, s := range sorted {
fmt.Printf(" %s: %.1f\n", s.Name, s.Score)
}
// 导出 JSON
jsonStr, _ := manager.ExportJSON()
fmt.Println("\nJSON 导出:")
fmt.Println(jsonStr)
}
3.5.2 切片性能优化对比
func slicePerformance() {
// 预分配容量(推荐)
start := time.Now()
s1 := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s1 = append(s1, i)
}
fmt.Printf("预分配容量耗时:%v\n", time.Since(start))
// 不预分配容量(慢)
start = time.Now()
s2 := make([]int, 0)
for i := 0; i < 10000; i++ {
s2 = append(s2, i)
}
fmt.Printf("不预分配容量耗时:%v\n", time.Since(start))
// 输出:预分配通常快 2-3 倍
}
3.6 常见陷阱与最佳实践
3.6.1 数组 vs 切片
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定 | 动态 |
| 类型 | 长度是类型一部分 | 无长度限制 |
| 传参 | 完整复制 | 只传描述符 |
| 使用场景 | 固定大小、性能敏感 | 通用、推荐 |
最佳实践:99% 的场景使用切片,数组仅在特殊场景(如性能极致优化、固定大小缓冲区)使用。
3.6.2 切片内存泄漏
// 错误:切片持有大数组引用
func badPractice(data []byte) []byte {
return data[:10] // 持有整个 data 的引用
}
// 正确:创建新切片
func goodPractice(data []byte) []byte {
result := make([]byte, 10)
copy(result, data[:10])
return result
}
// 或者
func goodPractice2(data []byte) []byte {
return append([]byte(nil), data[:10]...)
}
3.6.3 映射并发安全
// 错误:并发读写
var m = make(map[string]int)
func worker() {
for i := 0; i < 1000; i++ {
m["key"] = i // panic: concurrent map writes
}
}
// 正确:使用 sync.Mutex
var (
m = make(map[string]int)
mu sync.RWMutex
)
func workerSafe() {
for i := 0; i < 1000; i++ {
mu.Lock()
m["key"] = i
mu.Unlock()
}
}
// 或者使用 sync.Map
var sm sync.Map
func workerSyncMap() {
for i := 0; i < 1000; i++ {
sm.Store("key", i)
}
}
3.6.4 结构体最佳实践
- 使用指针接收者:除非结构体很小且不需要修改
- 避免嵌入指针:优先嵌入值,除非需要 nil 语义
- 使用标签:为 JSON、数据库等添加元数据
- 导出字段:首字母大写才能被外部访问
- 组合优于继承:通过嵌入实现代码复用
3.7 课后练习
- 切片操作:实现一个函数,移除切片中的重复元素
- 映射统计:统计字符串中每个字符出现的次数
- 结构体链式调用:为结构体实现链式调用方法
- 性能优化:对比预分配容量和不预分配的切片性能差异
- 并发安全:实现一个线程安全的计数器(使用映射)
- 综合项目:实现一个简单的待办事项管理器(支持增删改查、排序、导出)
3.8 下一步
完成本章后,你将进入第四章:函数与接口,深入学习 Go 的函数式编程特性、闭包、 defer、panic/recover 以及接口的底层实现。
代码仓库位置:https://giter.top/openclaw/test/tree/main/chapters/chapter-3
下一章预告:闭包的高级用法、defer 的执行顺序、接口的空接口与类型断言、接口底层实现