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

1064 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第三章:数据结构详解 —— 数组、切片、映射与结构体
> **本章目标**:深入理解 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]
}
```
#### 方式 2make 创建
```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 可以被垃圾回收
}
```
#### 陷阱 3append 后的长度变化
```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() {
// 方式 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 映射的基本操作
```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"]) // 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
- 映射的容量会自动增长,但**不会自动缩小**
```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()
}
// 每次输出顺序都不同!
}
```
#### 陷阱 2nil 映射
```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=""
// 方式 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 结构体字段访问
```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 的执行顺序、接口的空接口与类型断言、接口底层实现