Files
test/chapters/chapter-3-data-structures.md

24 KiB
Raw Blame History

第三章:数据结构详解 —— 数组、切片、映射与结构体

本章目标:深入理解 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]
}

方式 2make 创建

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 可以被垃圾回收
}

陷阱 3append 后的长度变化

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() {
    // 方式 1make 创建
    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"])  // 0int 的零值)
    
    // 遍历(无序!)
    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()
    }
    // 每次输出顺序都不同!
}

陷阱 2nil 映射

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=""
    
    // 方式 4new返回指针
    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 结构体最佳实践

  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 的执行顺序、接口的空接口与类型断言、接口底层实现