复合类型
# 第 5 章 复合类型:array / slice / map / struct / string
Go 的复合类型是工程中用得最频繁的核心——slice 是 90% 场景的容器,map 是 80% 场景的字典,struct 是 OOP 的载体。 关键词:array 定长、slice 三元组、map 引用语义、struct 字段对齐、string 不可变
# 目录介绍
- 5.1 本章学习目标
- 5.2 数组(array)
- 5.3 切片(slice)
- 5.4 映射(map)
- 5.5 结构体(struct)
- 5.6 字符串(string)的本质
- 5.7 综合示例:构建一个 LRU 缓存
- 5.8 本章底层原理(简介)
- 5.9 Go 新手陷阱 Top 5
- 5.10 思考题
- 5.11 推荐阅读
# 5.1 本章学习目标
- ✅ 能解释 slice 的三元组结构以及
append何时分配新底层数组 - ✅ 知道 map 的 5 个关键陷阱(nil 写、并发、不可寻址、随机遍历、零值取出)
- ✅ 能根据字段类型重排 struct 来省内存
- ✅ 知道
string不可变的"语言级保证"以及它的 unsafe 突破方式 - ✅ 能默写 LRU 缓存(map + 双向链表 + 容量上限)
# 5.2 数组(array)
# 5.2.1 定长性:长度是类型的一部分
var a [5]int // 长度 5,零值 [0 0 0 0 0]
var b [3]string // 长度 3,零值 ["" "" ""]
c := [4]int{1, 2, 3, 4}
d := [...]int{1, 2, 3} // 编译器自动推导长度为 3
2
3
4
重点:[5]int 与 [6]int 是两个完全不同的类型,不能互相赋值:
var a [5]int
var b [6]int
a = b // ❌ 编译错:cannot use b (type [6]int) as type [5]int
2
3
这与 C/C++ 的"数组退化为指针"截然不同。Go 数组的"长度即类型"让编译器在编译期就能做边界检查。
# 5.2.2 数组是值类型
a := [3]int{1, 2, 3}
b := a // 整体复制(不是共享)
b[0] = 99
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [99 2 3]
2
3
4
5
函数传参也是完整复制——一个 [1024]int 数组传参就是 8KB 复制。这就是为什么实践中我们几乎总是用 slice 而不是 array。
# 5.2.3 数组在 Go 中很少直接用
工程实践里,数组主要出现在:
- 哈希底层(如 SHA256 返回
[32]byte) - 固定形状的数学结构(如
[3][3]float64表示 3×3 矩阵) - 作为 slice 的"底层"被动出现(slice 内部就指向一个 array)
其他场景几乎都用 slice。记住这个比例:array : slice ≈ 5% : 95%。
# 5.3 切片(slice)
# 5.3.1 切片底层三元组:(ptr, len, cap)
切片不是数组,它是一个3 字段的小 struct:
slice struct (24 字节, 64-bit 平台):
┌─────────┬─────┬─────┐
│ ptr │ len │ cap │
└─────────┴─────┴─────┘
↓
[底层数组] [0] [1] [2] [3] [4] ... [cap-1]
←── len ──→
←──────── cap ────────→
2
3
4
5
6
7
8
ptr:指向底层数组的指针len:当前可访问的元素数(s[0..len-1]合法,越界 panic)cap:底层数组实际容量(s[len..cap-1]是预留空间)
获取它们:
s := []int{10, 20, 30}
fmt.Println(len(s), cap(s)) // 3 3
2
# 5.3.2 make 与字面量创建切片
4 种创建方式:
// 1. 字面量(最常用)
s1 := []int{1, 2, 3} // len=3, cap=3
// 2. make(类型, len)
s2 := make([]int, 5) // len=5, cap=5, 元素全是 0
// 3. make(类型, len, cap)(提前分配 cap,避免后续 append 频繁扩容)
s3 := make([]int, 0, 100) // len=0, cap=100
// 4. nil 切片
var s4 []int // len=0, cap=0, ptr=nil
2
3
4
5
6
7
8
9
10
11
nil 切片 vs 空切片:
var s1 []int // nil 切片
s2 := []int{} // 空切片(不是 nil)
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1), len(s2)) // 0 0(都是 0)
2
3
4
5
6
实务上两者行为几乎一样——都能 len()、能 for range、能 append,唯一区别是 == nil 比较。首选 var s []int(nil 切片)。
# 5.3.3 append 扩容算法
append 是 slice 的核心操作:
s := []int{1, 2, 3}
s = append(s, 4) // 单值追加
s = append(s, 5, 6, 7) // 多值追加
s = append(s, other...) // 拼接另一个 slice
2
3
4
关键:当 len == cap 时,append 会分配新的底层数组并复制:
s := make([]int, 3, 3)
fmt.Printf("ptr=%p len=%d cap=%d\n", s, len(s), cap(s))
// ptr=0xc0000... len=3 cap=3
s = append(s, 4)
fmt.Printf("ptr=%p len=%d cap=%d\n", s, len(s), cap(s))
// ptr=0xc0001... len=4 cap=6 ← 新地址,cap 翻倍
2
3
4
5
6
7
扩容策略(Go 1.18+ 简化版):
| 当前 cap | 新 cap |
|---|---|
| < 256 | 翻倍(2 倍) |
| ≥ 256 | 渐进收敛到 1.25 倍 |
精确公式见 runtime/slice.go: growslice。实务建议:能预估容量就用 make([]T, 0, n) 一次性分配,避免多次扩容拷贝。
// ❌ 多次扩容
ids := []int{}
for _, u := range users { ids = append(ids, u.ID) }
// ✅ 预分配
ids := make([]int, 0, len(users))
for _, u := range users { ids = append(ids, u.ID) }
2
3
4
5
6
7
# 5.3.4 子切片共享底层数组(陷阱重灾区)
arr := []int{1, 2, 3, 4, 5}
sub := arr[1:3] // [2 3],但 cap = 4(从 arr[1] 到 arr[4])
sub[0] = 999 // ⚠️ 同时修改了 arr!
fmt.Println(arr) // [1 999 3 4 5]
2
3
4
为什么 cap=4? 因为 sub 共享 arr 的底层数组,从下标 1 起到末尾,剩下 4 个槽位。
更隐蔽的坑——子切片 append 可能覆盖原 slice:
arr := []int{1, 2, 3, 4, 5}
sub := arr[1:3] // [2 3],cap=4
sub = append(sub, 999) // 没扩容(cap 够),写入 arr[3]
fmt.Println(arr) // [1 2 3 999 5] ← arr 被污染!
2
3
4
修复办法 1:复制独立:
sub := append([]int(nil), arr[1:3]...) // 独立的新底层数组
// 或 Go 1.21+
sub := slices.Clone(arr[1:3])
2
3
修复办法 2:三索引限制 cap(见下节)。
# 5.3.5 三索引 s[i:j:k] 限制 cap
arr := []int{1, 2, 3, 4, 5}
sub := arr[1:3:3] // len=2, cap=2(k=3 限制 cap = k-i = 2)
sub = append(sub, 999) // 必扩容到新数组,不会影响 arr
fmt.Println(arr) // [1 2 3 4 5]
2
3
4
s[i:j:k] 的含义:len = j-i,cap = k-i。这是"逃避共享底层数组陷阱"的最干净方式。返回子切片给外部时,强烈建议用三索引。
# 5.3.6 copy 与"深拷贝"
copy(dst, src) 复制元素,返回实际复制的元素数(min(len(dst), len(src))):
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // n = 3, dst = [1 2 3]
2
3
经典模式:
// 1. 完整克隆
clone := make([]int, len(src))
copy(clone, src)
// 或 Go 1.21+
clone := slices.Clone(src)
// 2. 删除中间元素(保持顺序)
i := 2
src = append(src[:i], src[i+1:]...)
// 或 Go 1.21+
src = slices.Delete(src, i, i+1)
2
3
4
5
6
7
8
9
10
11
⚠️ 注意:copy 只做浅拷贝——如果元素是指针/slice/map,复制的是引用,不是底层数据。深拷贝需要自己写或借助库(encoding/gob、json.Marshal/Unmarshal 等)。
# 5.4 映射(map)
# 5.4.1 make(map[K]V) 与字面量
// 1. make
m1 := make(map[string]int) // 空 map,可写
m2 := make(map[string]int, 100) // 提示 hint cap,减少扩容
// 2. 字面量
m3 := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
// 3. nil map(只读)
var m4 map[string]int // nil
fmt.Println(m4["any"]) // 0(读 nil map 合法,返回零值)
m4["x"] = 1 // ❌ panic: assignment to entry in nil map
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Key 类型必须可比较(支持 ==)。所以 slice/map/func 不能做 key;指针、interface、struct(含可比较字段)可以。
# 5.4.2 双返回值取值 v, ok := m[k]
map 索引有两种返回形式:
m := map[string]int{"a": 1}
// 单值:key 不存在时返回**零值**,无法区分"没有"和"是 0"
v := m["a"] // 1
v = m["zzz"] // 0
// 双值:ok 表示是否存在
v, ok := m["a"] // v=1, ok=true
v, ok = m["zzz"] // v=0, ok=false
2
3
4
5
6
7
8
9
最佳实践:除非你确定值的零值不可能是合法值,否则永远用双返回值。
# 5.4.3 delete 与 nil map 写入 panic
m := map[string]int{"a": 1}
delete(m, "a") // 安全,无返回值
delete(m, "zzz") // 删不存在的 key 也不会 panic
var m2 map[string]int
m2["x"] = 1 // ❌ panic
2
3
4
5
6
最常见 bug:在 struct 字段上忘了 make:
type Cache struct {
data map[string]int // 未初始化
}
c := Cache{}
c.data["x"] = 1 // ❌ panic
2
3
4
5
6
修复:在构造函数里 make:
func NewCache() *Cache {
return &Cache{data: make(map[string]int)}
}
2
3
# 5.4.4 map 遍历顺序随机(设计意图)
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v)
}
// 第一次跑:c 3 / a 1 / b 2
// 第二次跑:b 2 / c 3 / a 1
2
3
4
5
6
Go 故意每次启动都用不同随机种子,让你不能依赖 map 遍历顺序——这是为了避免代码"看起来稳定但其实暗中依赖某次哈希结果",迁移到不同 Go 版本就坏。
需要稳定顺序?把 key 提取出来排序:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
2
3
4
5
6
7
8
或 Go 1.21+:
import "slices"
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys { ... }
2
3
4
# 5.4.5 map 不可寻址(不能 &m["key"])
m := map[string]int{"a": 1}
p := &m["a"] // ❌ 编译错:cannot take the address of m["a"]
2
为什么? map 内部为了支持扩容(rehash),元素的内存位置随时可能变。如果允许取地址,扩容后这个指针就成了野指针。Go 选择直接禁止取址。
典型坑——struct 值类型:
type User struct {
Name string
Age int
}
m := map[string]User{"u1": {"Alice", 20}}
m["u1"].Age = 21 // ❌ 编译错:cannot assign to struct field m["u1"].Age in map
2
3
4
5
6
7
修复:要么改用指针,要么取出来改回去:
// 方案 A:value 用指针
m := map[string]*User{"u1": {"Alice", 20}}
m["u1"].Age = 21 // ✅
// 方案 B:取出 → 改 → 写回
u := m["u1"]
u.Age = 21
m["u1"] = u
2
3
4
5
6
7
8
# 5.4.6 并发读写 map 直接 fatal(不是 panic)
m := map[int]int{}
go func() { for { m[1] = 1 } }()
go func() { for { _ = m[1] } }()
time.Sleep(time.Second)
// fatal error: concurrent map read and map write
2
3
4
5
6
注意是 fatal,不是 panic——recover 救不回来!这是 Go runtime 在检测到并发读写时直接调用 fatalthrow。
修复方案:
| 方案 | 适用场景 |
|---|---|
sync.Mutex 全锁 | key 数量不大、读写都频繁 |
sync.RWMutex | 读多写少 |
sync.Map | key 集合稳定、读多写少(key 反复增删则慢) |
| 分片 map(sharded map) | 高并发热点 |
详见 第 14 章 同步 sync 包。
# 5.5 结构体(struct)
# 5.5.1 字段定义与初始化(命名 vs 位置)
type User struct {
ID int
Name string
Age int
}
// 命名初始化(强烈推荐)
u1 := User{ID: 1, Name: "Alice", Age: 20}
// 位置初始化(不推荐,字段顺序变了就 broken)
u2 := User{1, "Alice", 20}
// 部分初始化(其他字段取零值)
u3 := User{Name: "Bob"} // ID=0, Age=0
// 零值 struct
var u4 User // ID=0, Name="", Age=0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
永远命名初始化——位置初始化在 struct 加新字段时所有调用点都要改。
# 5.5.2 嵌入字段(无字段名)
type Base struct {
ID int
CTime time.Time
}
type User struct {
Base // 嵌入字段:没有显式字段名
Name string
}
u := User{
Base: Base{ID: 1, CTime: time.Now()},
Name: "Alice",
}
fmt.Println(u.ID) // 字段提升:直接访问 ID(不必 u.Base.ID)
fmt.Println(u.Base.ID) // 也可以
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
嵌入字段是 Go 实现"代码复用"的方式,不是继承——详见 第 9 章。
# 5.5.3 Tag:反射的元数据
type User struct {
ID int `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:64;not null"`
Age int `json:"age,omitempty"`
}
2
3
4
5
- Tag 是反引号包起来的字符串,紧跟在字段类型后
- 多个 Tag 用空格分隔,惯例
key:"value"格式 - 反射通过
reflect.StructTag.Get("json")取出
encoding/json、gorm、validator 等库严重依赖 Tag。常见的 json Tag 选项:
| Tag | 含义 |
|---|---|
json:"name" | JSON 里键名为 name |
json:"-" | 不参与序列化 |
json:",omitempty" | 零值则忽略 |
json:"name,string" | 序列化为字符串(即使是 int) |
# 5.5.4 字段对齐与省内存技巧
// 16 字节
type Bad struct {
a bool // 1 字节 + 7 字节 padding
b int64 // 8 字节
}
// 9 字节(实际占用根据对齐算 16 字节,但更紧凑)
type Good struct {
b int64 // 8 字节
a bool // 1 字节 + 7 字节 padding
}
// 含 3 个 bool 时差异更明显
type Wasteful struct { // 24 字节
a bool; b int64; c bool
}
type Compact struct { // 16 字节
b int64; a, c bool
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
规则:把大字段放前面、bool/byte 这类小字段聚在末尾。go vet -fieldalignment 工具会自动检测。
➡ 卷三第 3 章 结构体与对齐 详讲对齐规则。
# 5.5.5 == 可比较性规则
| 字段类型 | struct 是否可比较 |
|---|---|
| 全部基础类型(int、string、bool) | ✅ |
| 数组(元素类型可比较) | ✅ |
| 嵌套 struct(字段全可比较) | ✅ |
| 含指针字段 | ✅(比的是指针值,不是指向的内容) |
| 含接口字段 | ✅(运行时比较底层类型 + 值,不可比时 panic) |
| 含 slice / map / func 字段 | ❌ 编译错 |
type A struct {
Name string
Age int
}
a1 := A{"X", 1}
a2 := A{"X", 1}
fmt.Println(a1 == a2) // true ✅
type B struct {
Tags []string
}
var b1, b2 B
fmt.Println(b1 == b2) // ❌ 编译错:struct containing []string cannot be compared
2
3
4
5
6
7
8
9
10
11
12
13
如何深比较含 slice 的 struct?用 reflect.DeepEqual(性能差)或自己写 Equal 方法。
# 5.6 字符串(string)的本质
# 5.6.1 不可变只读字节序列
string 在 Go 里是只读字节切片,底层结构与 slice 类似但少了 cap:
string struct (16 字节):
┌─────────┬─────┐
│ ptr │ len │
└─────────┴─────┘
↓
[只读 UTF-8 字节] ...
2
3
4
5
6
关键事实:
s := "hello, 世界"
fmt.Println(len(s)) // 13(字节数,不是字符数)
fmt.Println(s[0]) // 104('h' 的字节,类型是 byte)
fmt.Println(string(s[0])) // "h"
// 中文每字符 3 字节(UTF-8)
fmt.Println(s[7]) // 228("世"的第一字节,单看没意义)
// 正确遍历字符(rune)
for i, r := range s {
fmt.Printf("byte %d: %c (%d)\n", i, r, r)
}
// byte 0: h (104)
// byte 1: e (101)
// ...
// byte 7: 世 (19990)
// byte 10: 界 (30028)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
记住:s[i] 是 byte(字节),for i, r := range s 给的 r 才是 rune(Unicode 码点)。
字符串拼接最好用 strings.Builder 而不是 +:
// ❌ 每次 + 都分配新字符串
result := ""
for _, p := range parts {
result += p // O(n²) 内存分配
}
// ✅ 预分配 + 一次性拼接
var b strings.Builder
b.Grow(estimateSize)
for _, p := range parts {
b.WriteString(p)
}
result := b.String()
2
3
4
5
6
7
8
9
10
11
12
13
# 5.6.2 字符串与切片互转的零拷贝优化(Go 1.20+)
// 标准转换(有内存拷贝)
b := []byte(s)
s2 := string(b)
2
3
string([]byte) 与 []byte(string) 默认都会复制底层数据——因为 string 必须保证不可变,而 []byte 可变。
Go 1.20+ 引入零拷贝 API(仅在编译器能证明安全时使用):
import "unsafe"
// Go 1.20+:[]byte 转 string 零拷贝
b := []byte("hello")
s := unsafe.String(&b[0], len(b))
// string 转 []byte 零拷贝
data := unsafe.Slice(unsafe.StringData(s), len(s))
2
3
4
5
6
7
8
⚠️ 使用前提:转换后承诺不会修改 byte 数据——一旦修改,破坏 string 不可变性,行为未定义。仅在性能热点用。
➡ 卷三第 4 章 字符串与切片底层 详讲。
# 5.7 综合示例:构建一个 LRU 缓存
把本章的 slice / map / struct 串起来,写一个 100 行的 LRU 缓存——它是面试高频题,也是 Redis 内部用的算法之一。
// lrucache/cache.go
package lrucache
// node 双向链表节点
type node struct {
key string
value any
prev, next *node
}
// LRU 是基于 map + 双向链表的最近最少使用缓存
type LRU struct {
cap int
items map[string]*node
head, tail *node // head 是最近用、tail 是最久未用
}
// New 创建一个容量为 cap 的 LRU
func New(cap int) *LRU {
if cap <= 0 {
panic("lrucache: cap must be > 0")
}
head := &node{}
tail := &node{}
head.next = tail
tail.prev = head
return &LRU{
cap: cap,
items: make(map[string]*node, cap),
head: head,
tail: tail,
}
}
// Get 取值,若存在则提升到最近使用
func (c *LRU) Get(key string) (any, bool) {
n, ok := c.items[key]
if !ok {
return nil, false
}
c.moveToFront(n)
return n.value, true
}
// Put 写入值,超容则淘汰最久未用
func (c *LRU) Put(key string, value any) {
if n, ok := c.items[key]; ok {
n.value = value
c.moveToFront(n)
return
}
n := &node{key: key, value: value}
c.items[key] = n
c.addToFront(n)
if len(c.items) > c.cap {
evict := c.tail.prev
c.remove(evict)
delete(c.items, evict.key)
}
}
// Len 当前条目数
func (c *LRU) Len() int { return len(c.items) }
// --- 内部链表操作 ---
func (c *LRU) addToFront(n *node) {
n.prev = c.head
n.next = c.head.next
c.head.next.prev = n
c.head.next = n
}
func (c *LRU) remove(n *node) {
n.prev.next = n.next
n.next.prev = n.prev
n.prev, n.next = nil, nil
}
func (c *LRU) moveToFront(n *node) {
c.remove(n)
c.addToFront(n)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
测试:
// lrucache/cache_test.go
package lrucache
import "testing"
func TestLRU(t *testing.T) {
c := New(2)
c.Put("a", 1)
c.Put("b", 2)
if v, ok := c.Get("a"); !ok || v != 1 {
t.Fatalf("expected a=1, got %v ok=%v", v, ok)
}
c.Put("c", 3) // 此时 b 是最久未用,应被淘汰
if _, ok := c.Get("b"); ok {
t.Fatal("b should be evicted")
}
if v, _ := c.Get("c"); v != 3 {
t.Fatalf("expected c=3, got %v", v)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
跑测试:
$ go test -v ./...
=== RUN TestLRU
--- PASS: TestLRU (0.00s)
PASS
2
3
4
用到的本章知识点:
| 知识点 | 在哪一行 |
|---|---|
| struct 定义 | type node struct { ... } |
| 指针字段 | prev, next *node |
| map | items map[string]*node |
make(map[K]V, hint) | make(map[string]*node, cap) |
| 双返回值 | n, ok := c.items[key] |
delete(map, key) | delete(c.items, evict.key) |
| 多个返回值赋值 | n.prev, n.next = nil, nil |
空接口 any | value any |
# 5.8 本章底层原理(简介)
| 类型 | 内部结构 | 详解卷三 |
|---|---|---|
| slice | runtime.slice {ptr, len, cap} | 第 4 章 |
| map | runtime.hmap + bmap 桶数组 | 第 10 章 |
| string | runtime.stringHeader {ptr, len} | 第 4 章 |
| struct | 按字段顺序连续布局 + padding | 第 3 章 |
| array | 固定长度连续内存 | 第 1 章 |
# 5.9 Go 新手陷阱 Top 5
# ❌ 陷阱 1:函数内 append(s, x) 不返回赋值
func add(s []int, v int) {
s = append(s, v) // ❌ 调用方看不到变化(如果发生扩容)
}
s := []int{1, 2}
add(s, 3)
fmt.Println(s) // [1 2],不是 [1 2 3]
2
3
4
5
6
7
根因:slice 是值(三元组),传入函数是复制;扩容后内部 ptr 指向新数组,外部依然指向旧数组。
修复:要么返回新 slice,要么传 *[]int:
func add(s []int, v int) []int { return append(s, v) }
s = add(s, 3) // ✅
2
3
# ❌ 陷阱 2:用子切片 s[1:3] 后修改它,原 slice 也变
详见 §5.3.4 / §5.3.5。返回切片给外部时一律用三索引 s[i:j:k] 或 slices.Clone。
# ❌ 陷阱 3:给 nil map 赋值 panic
type Cache struct {
data map[string]int // 忘了在构造函数里 make
}
c := &Cache{}
c.data["x"] = 1 // ❌ panic
2
3
4
5
修复:永远在构造函数里 make,或用 sync.Map(但有性能代价)。
# ❌ 陷阱 4:循环里 &v 共享地址(Go 1.21 及之前)
// Go 1.21 及之前
ps := []*int{}
for _, v := range []int{1, 2, 3} {
ps = append(ps, &v) // 所有元素指向同一个 v
}
fmt.Println(*ps[0], *ps[1], *ps[2]) // 3 3 3
2
3
4
5
6
Go 1.22 起循环变量每轮新建,此问题消失。但老代码、老服务还在的话,仍然要懂这个坑。修复(兼容老版本):
for _, v := range []int{1, 2, 3} {
v := v // 显式 shadow,构造每轮独立变量
ps = append(ps, &v)
}
2
3
4
# ❌ 陷阱 5:含 slice/map/func 字段的 struct 用 == 比较
type Conf struct {
Tags []string
}
var a, b Conf
fmt.Println(a == b) // ❌ 编译错
2
3
4
5
6
修复:
- 用
reflect.DeepEqual(a, b)(性能差) - 自己写
func (c Conf) Equal(o Conf) bool - 用
slices.Equal(a.Tags, b.Tags) && ...字段级比较(Go 1.21+)
# 5.10 思考题
- 为什么
[5]int与[6]int是两个不同类型?这种"长度即类型"的设计带来什么好处? - 写一段代码验证:
make([]int, 0, 100)和make([]int, 100)在内存占用与len(s)上有什么差别? - 一个 slice
s已经cap=100, len=50,再s = append(s, x)会扩容吗?什么情况下会?什么情况下不会? - 为什么 Go 选择让
delete(m, "noexist")静默成功,而m[nilkey] = 1(nil map 写入)panic?这种"读宽松、写严格"哲学是否一致? - 写一个函数
Unique(s []int) []int,去重并保持原顺序。提示:用map[int]struct{}做"集合"。 - 把 5.7 的 LRU 改造为支持 TTL(每个条目可指定过期时间)。提示:每个 node 加
expireAt time.Time,Get 时若过期则删掉。 - 一个 1000 字节的 struct 数组
[]big,遍历时for _, b := range arr与for i := range arr; b := &arr[i]性能差距多大?为什么?
# 5.11 推荐阅读
# 卷内
- 卷一第 7 章 函数(slice 作为参数)
- 卷一第 9 章 结构体与方法(struct 接收者)
- 卷一第 14 章 同步 sync 包(map 并发安全)
- 卷一第 16 章 标准库与泛型(
slices/maps包)