# 第三章:数据结构详解 —— 数组、切片、映射与结构体 > **本章目标**:深入理解 Go 的四种核心数据结构,掌握数组与切片的本质区别、映射的底层实现、结构体的组合与嵌入,以及在实际开发中的最佳实践。 ## 3.1 数组(Arrays):固定长度的序列 ### 3.1.1 数组的基本概念 数组是**固定长度**的相同类型元素的集合。一旦声明,长度不可改变。 ```go 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 数组的访问与遍历 ```go 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 多维数组 ```go 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:值复制性能问题** ```go func arrayCopyDemo() { arr := [10000]int{} // 初始化... // 传参时会复制整个数组(性能差) processArray(arr) } func processArray(a [10000]int) { // 这里接收的是副本 } // 正确做法:传指针 func processArrayPtr(a *[10000]int) { // 只传递地址 } ``` **问题 2:长度固定** ```go arr := [5]int{1, 2, 3, 4, 5} // arr[5] = 6 // 编译错误!数组长度固定 ``` **结论**:在实际开发中,**极少直接使用数组**,更多使用**切片(Slice)**。 --- ## 3.2 切片(Slices):动态长度的视图 切片是 Go 中最常用的数据结构,它是**对数组的动态视图**,提供了灵活的长度和容量管理。 ### 3.2.1 切片的底层结构 切片不是数组,它是一个**描述符**,包含三个字段: - `ptr`:指向底层数组的指针 - `len`:切片长度 - `cap`:切片容量(从 ptr 开始到数组末尾的长度) ```go type SliceHeader struct { Data uintptr // 指向底层数组 Len int // 长度 Cap int // 容量 } ``` ### 3.2.2 切片的创建 #### 方式 1:从数组创建 ```go 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 创建 ```go 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:字面量创建 ```go 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:追加元素 ```go 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:复制切片 ```go 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) ```go 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` 创建新切片 ```go // 释放内存的正确方式 func trimMemory(s []int) []int { result := append([]int(nil), s[:3]...) // 创建新切片,只包含前 3 个元素 return result } ``` ### 3.2.4 切片的常见陷阱 #### 陷阱 1:共享底层数组 ```go 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:容量陷阱导致内存泄漏 ```go func capacityLeak() { data := make([]int, 1000000) // 大数组 // 处理数据... // 错误:只取前 10 个,但容量仍为 1000000 small := data[:10] // small 持有整个大数组的引用,内存无法释放 // 正确:创建新切片 small := append([]int(nil), data[:10]...) // 现在 data 可以被垃圾回收 } ``` #### 陷阱 3:append 后的长度变化 ```go 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 切片的高级用法 #### 使用切片作为函数参数 ```go 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 那样的列表推导式,但可以用循环实现: ```go 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 映射的创建与初始化 ```go 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 映射的基本操作 ```go 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 - 映射的容量会自动增长,但**不会自动缩小** ```go // 并发安全的映射 func concurrentMap() { var m sync.Map // 或使用 sync.RWMutex 保护普通 map m.Store("key", "value") val, _ := m.Load("key") fmt.Println(val) } ``` ### 3.3.4 映射的常见陷阱 #### 陷阱 1:遍历无序 ```go 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 映射 ```go 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:映射作为函数参数 ```go 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 高级用法:嵌套映射 ```go 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 结构体的定义与初始化 ```go 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 结构体字段访问 ```go 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 结构体方法 ```go 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 没有继承,但通过**嵌入**实现类似功能。 ```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、数据库映射等。 ```go 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 结构体比较 ```go 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 学生管理系统 ```go 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 切片性能优化对比 ```go 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 切片内存泄漏 ```go // 错误:切片持有大数组引用 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 映射并发安全 ```go // 错误:并发读写 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 结构体最佳实践 1. **使用指针接收者**:除非结构体很小且不需要修改 2. **避免嵌入指针**:优先嵌入值,除非需要 nil 语义 3. **使用标签**:为 JSON、数据库等添加元数据 4. **导出字段**:首字母大写才能被外部访问 5. **组合优于继承**:通过嵌入实现代码复用 --- ## 3.7 课后练习 1. **切片操作**:实现一个函数,移除切片中的重复元素 2. **映射统计**:统计字符串中每个字符出现的次数 3. **结构体链式调用**:为结构体实现链式调用方法 4. **性能优化**:对比预分配容量和不预分配的切片性能差异 5. **并发安全**:实现一个线程安全的计数器(使用映射) 6. **综合项目**:实现一个简单的待办事项管理器(支持增删改查、排序、导出) ## 3.8 下一步 完成本章后,你将进入第四章:**函数与接口**,深入学习 Go 的函数式编程特性、闭包、 defer、panic/recover 以及接口的底层实现。 --- **代码仓库位置**:https://giter.top/openclaw/test/tree/main/chapters/chapter-3 **下一章预告**:闭包的高级用法、defer 的执行顺序、接口的空接口与类型断言、接口底层实现