基础篇讲了数组、切片、映射的基本操作。这篇进阶篇聚焦三个方向:多维容器结构体与容器结合并发安全容器


🧊 一、多维容器操作

1.1 二维切片:矩阵初始化

1
2
3
4
5
6
7
8
9
10
rows, cols := 3, 4

// ⚠️ 每行都要单独 make!只 make 外层的话内层是 nil
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}

matrix[0][0] = 1
matrix[2][3] = 99

💥 常见 panicmake([][]int, 3) 后直接 matrix[0][0] = 1 —— 内层是 nil!

1.2 锯齿数组(各行长度不同)

切片独有,数组做不到:

1
2
3
4
5
6
7
8
9
10
11
jagged := [][]int{
{1, 2, 3}, // 3 个
{4, 5}, // 2 个
{6, 7, 8, 9, 10}, // 5 个
}

// 动态追加新行
jagged = append(jagged, []int{11, 12})

// 动态往某一行追加
jagged[1] = append(jagged[1], 6) // [4 5 6]

1.3 深拷贝

直接 copy 外层只拷贝了引用,内层仍然共享:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src := [][]int{{1, 2}, {3, 4}}

// ❌ 浅拷贝
shallow := make([][]int, len(src))
copy(shallow, src)
shallow[0][0] = 99
fmt.Println(src[0][0]) // 99 ← 源数据被改了

// ✅ 深拷贝 — 逐行 copy
deep := make([][]int, len(src))
for i := range src {
deep[i] = make([]int, len(src[i]))
copy(deep[i], src[i])
}
deep[0][0] = 999
fmt.Println(src[0][0]) // 99 ← 安全

1.4 切片去重

方法一:排序后去重(会改变顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
func unique(s []int) []int {
sort.Ints(s)
j := 0
for i := 1; i < len(s); i++ {
if s[i] != s[j] {
j++
s[j] = s[i]
}
}
return s[:j+1]
}

unique([]int{3, 1, 2, 3, 1}) // [1 2 3]

方法二:用 map 去重(保持首次出现顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
func uniqueMap(s []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(s))
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}

uniqueMap([]int{3, 1, 2, 3, 1}) // [3 1 2]

1.5 切片反转

1
2
3
4
5
6
7
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

reverse([]int{1, 2, 3, 4, 5}) // [5 4 3 2 1]

🏗️ 二、结构体与容器结合

2.1 结构体切片 — “对象数组”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Student struct {
Name string
Age int
Score float64
}

students := []Student{
{"Alice", 20, 95.5},
{"Bob", 21, 88.0},
{"Charlie", 19, 92.3},
}

// 追加
students = append(students, Student{"Diana", 22, 97.0})

2.2 结构体切片排序

1
2
3
4
5
6
7
8
9
10
11
12
// 按分数降序
sort.Slice(students, func(i, j int) bool {
return students[i].Score > students[j].Score
})

// 多条件:先分数降序,相同则年龄升序
sort.Slice(students, func(i, j int) bool {
if students[i].Score != students[j].Score {
return students[i].Score > students[j].Score
}
return students[i].Age < students[j].Age
})

2.3 查找与过滤

查找(返回指针,可直接修改原数据):

1
2
3
4
5
6
7
8
9
10
11
12
13
func findByName(students []Student, name string) *Student {
for i := range students {
if students[i].Name == name {
return &students[i]
}
}
return nil
}

// 使用
if s := findByName(students, "Bob"); s != nil {
s.Score = 90.0 // 直接修改
}

过滤:

1
2
3
4
5
6
7
8
9
10
11
func filterByScore(students []Student, min float64) []Student {
result := make([]Student, 0)
for _, s := range students {
if s.Score >= min {
result = append(result, s)
}
}
return result
}

excellent := filterByScore(students, 90.0)

2.4 map 存结构体 vs 指针

存值 — 不能直接修改字段:

1
2
3
4
5
6
7
8
9
m := map[string]Student{"Alice": {"Alice", 20, 95.5}}

// ❌ 编译错误:cannot assign to struct field
// m["Alice"].Score = 99

// ✅ 必须先取出、修改、再存回
s := m["Alice"]
s.Score = 99
m["Alice"] = s

存指针 — 可以直接修改(推荐):

1
2
3
4
5
6
m := map[string]*Student{
"Alice": {"Alice", 20, 95.5},
}

// ✅ 直接修改
m["Alice"].Score = 99

2.5 切片转 map(泛型通用写法)

需要频繁按某个字段查找时,把切片转成 map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 泛型工具函数(Go 1.18+)
func ToMap[K comparable, V any](slice []V, keyFn func(V) K) map[K]V {
m := make(map[K]V, len(slice))
for _, v := range slice {
m[keyFn(v)] = v
}
return m
}

// 使用
studentMap := ToMap(students, func(s Student) string {
return s.Name
})

// O(1) 查找
if s, ok := studentMap["Alice"]; ok {
fmt.Println(s.Score)
}

🔒 三、并发安全容器

Go 内置的 map 和切片都不是并发安全的,多 goroutine 同时读写会 panic。

3.1 方案一:RWMutex + map(最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}

func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock() // 写锁 — 互斥
defer sm.mu.Unlock()
sm.data[key] = value
}

func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock() // 读锁 — 多个读者可并发
defer sm.mu.RUnlock()
val, ok := sm.data[key]
return val, ok
}

func (sm *SafeMap) Delete(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.data, key)
}

💡 RWMutex 的好处:读锁可以同时被多个 goroutine 持有,读多写少时性能更好。

3.2 方案二:sync.Map(标准库)

Go 1.9 引入,适合读多写少键只增不改的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var m sync.Map

// 存储
m.Store("Go", 1)
m.Store("Java", 2)

// 读取(需要类型断言)
val, ok := m.Load("Go")
if ok {
fmt.Println(val.(int))
}

// 读取或存储(原子操作)
val, loaded := m.LoadOrStore("Rust", 4)
// loaded = false → 新存入
// loaded = true → key 已存在,返回已有值

// 删除
m.Delete("Java")

// 遍历
m.Range(func(key, value any) bool {
fmt.Printf("%s => %d\n", key.(string), value.(int))
return true // false 停止遍历
})

3.3 选型建议

场景推荐方案
通用(读写均衡)RWMutex + map
读多写少sync.Map
键只增不改(缓存)sync.Map
需要类型安全RWMutex + map
需要有序遍历RWMutex + map

💡 大多数情况用 RWMutex + map 就够了,代码更直观,无类型断言。

3.4 并发安全切片

1
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
type SafeSlice struct {
mu sync.RWMutex
data []string
}

func (ss *SafeSlice) Append(item string) {
ss.mu.Lock()
defer ss.mu.Unlock()
ss.data = append(ss.data, item)
}

func (ss *SafeSlice) Get(i int) (string, bool) {
ss.mu.RLock()
defer ss.mu.RUnlock()
if i < 0 || i >= len(ss.data) {
return "", false
}
return ss.data[i], true
}

// 返回副本,防止外部修改
func (ss *SafeSlice) Snapshot() []string {
ss.mu.RLock()
defer ss.mu.RUnlock()
cp := make([]string, len(ss.data))
copy(cp, ss.data)
return cp
}

3.5 实战:多 goroutine 统计词频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
counts := make(map[string]int)
var mu sync.Mutex
var wg sync.WaitGroup

words := []string{"apple", "banana", "apple", "cherry", "banana", "apple"}

for _, word := range words {
wg.Add(1)
go func(w string) {
defer wg.Done()
mu.Lock()
counts[w]++
mu.Unlock()
}(word)
}

wg.Wait()
// apple: 3, banana: 2, cherry: 1

🎮 四、综合运用:用户管理器

把切片、map、结构体、并发安全融合在一起:

1
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
type User struct {
ID int
Name string
Score int
}

type UserManager struct {
mu sync.RWMutex
users map[int]*User // 按 ID 快速查找
scores []int // 分数列表(统计用)
}

// 添加用户
func (um *UserManager) AddUser(u *User) {
um.mu.Lock()
defer um.mu.Unlock()
um.users[u.ID] = u
um.scores = append(um.scores, u.Score)
}

// 查找
func (um *UserManager) GetUser(id int) (*User, bool) {
um.mu.RLock()
defer um.mu.RUnlock()
u, ok := um.users[id]
return u, ok
}

// Top N 排名
func (um *UserManager) TopN(n int) []*User {
um.mu.RLock()
defer um.mu.RUnlock()

all := make([]*User, 0, len(um.users))
for _, u := range um.users {
all = append(all, u)
}
sort.Slice(all, func(i, j int) bool {
return all[i].Score > all[j].Score
})
if n > len(all) {
n = len(all)
}
return all[:n]
}

// 平均分
func (um *UserManager) AvgScore() float64 {
um.mu.RLock()
defer um.mu.RUnlock()
if len(um.scores) == 0 {
return 0
}
sum := 0
for _, s := range um.scores {
sum += s
}
return float64(sum) / float64(len(um.scores))
}

使用:

1
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
um := NewUserManager()

// 并发添加
var wg sync.WaitGroup
users := []*User{
{1, "Alice", 95}, {2, "Bob", 88}, {3, "Charlie", 92},
}
for _, u := range users {
wg.Add(1)
go func(user *User) {
defer wg.Done()
um.AddUser(user)
}(u)
}
wg.Wait()

// 查询
if u, _ := um.GetUser(1); u != nil {
fmt.Printf("%s: %d分\n", u.Name, u.Score)
}

// Top 3
for i, u := range um.TopN(3) {
fmt.Printf("%d. %s — %d分\n", i+1, u.Name, u.Score)
}

// 平均分
fmt.Printf("平均分: %.1f\n", um.AvgScore())

🎯 五、实战小贴士

#要点
1多维切片每行都要单独 make
2深拷贝多维切片要逐行 copy
3结构体 + map 用指针 map[K]*V 才能直接修改
4并发 map 默认 RWMutex + map,读多写少再选 sync.Map
5sort.Slice 是结构体切片排序利器
6切片去重/过滤没有内置函数,自己写或泛型封装