1064 lines
24 KiB
Markdown
1064 lines
24 KiB
Markdown
# 第三章:数据结构详解 —— 数组、切片、映射与结构体
|
||
|
||
> **本章目标**:深入理解 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 的执行顺序、接口的空接口与类型断言、接口底层实现
|