章节

11.4 练习

本篇通过若干分级练习巩固原子操作、互斥锁与 sync.Map,并能写出基础并发场景下的线程安全代码。

练习

学完本章你应该掌握

  • 能识别哪些并发场景会发生数据竞争,并知道共享变量、切片、map 在并发写入时都需要同步保护。
  • 能使用 sync/atomic 安全地维护简单计数器,并理解为什么 count++ 在并发场景下不可靠。
  • 能使用 sync.Mutex 保护共享临界区,写对 LockUnlockdefer 的基本范围。
  • 能使用 sync.Map 完成并发安全的存取、删除、遍历,并在取值后做必要的类型断言。
  • 能把前面学过的结构体、方法、切片、map、函数、WaitGroup、channel、select 等知识自然组合到线程安全场景里。

简单

第 1 题:用原子操作统计完成的练习数

第 1 题 简单

编写一个 Go 程序,完成下面要求:

  • 准备切片:
1
tasks := []string{"变量", "切片", "函数", "接口"}
  • 为每个任务启动一个 goroutine
  • 每个 goroutine 输出 xxx 完成
  • 使用 sync/atomic 安全统计完成数量
  • 使用 sync.WaitGroup 等待所有 goroutine 结束
  • 最后输出:
1
finished=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
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	tasks := []string{"变量", "切片", "函数", "接口"}

	var finished int64
	var wg sync.WaitGroup

	for _, task := range tasks {
		wg.Add(1)
		go func(task string) {
			defer wg.Done()
			fmt.Println(task, "完成")
			atomic.AddInt64(&finished, 1)
		}(task)
	}

	wg.Wait()
	fmt.Printf("finished=%d\n", finished)
}

说明:前面每一行 xxx 完成 的输出顺序不固定,但最终的 finished 应该稳定等于 4

第 2 题:修正并发点赞数代码

第 2 题 简单

下面程序的目标是稳定输出:

1
likes=100

但它现在有线程安全问题。请改正后让它正常运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"fmt"
	"sync"
)

func main() {
	var likes int64
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			likes++
		}()
	}

	wg.Wait()
	fmt.Printf("likes=%d\n", likes)
}
查看参考答案

修正后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var likes int64
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			atomic.AddInt64(&likes, 1)
		}()
	}

	wg.Wait()
	fmt.Printf("likes=%d\n", likes)
}

关键点:

  • likes++ 不是原子操作,它包含“读取旧值、加 1、写回新值”多个步骤。
  • 多个 goroutine 同时执行这几个步骤时,就可能把彼此的结果覆盖掉。
  • 这里的共享数据只是一个简单计数器,用 atomic.AddInt64 最直接。

第 3 题:给购物车总价加互斥锁

第 3 题 简单

编写一个 Go 程序,完成下面要求:

  • 定义结构体 Cart
  • 字段包括: mu sync.Mutex Total int
  • 给它绑定方法 Add(price int)
  • Add 里用互斥锁保护 Total
  • main 中启动两个 goroutine,分别调用: cart.Add(99) cart.Add(71)
  • 等两个 goroutine 都结束后,输出:
1
total=170
查看参考答案
 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
package main

import (
	"fmt"
	"sync"
)

type Cart struct {
	mu    sync.Mutex
	Total int
}

func (c *Cart) Add(price int) {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.Total += price
}

func main() {
	var wg sync.WaitGroup
	cart := &Cart{}

	wg.Add(2)

	go func() {
		defer wg.Done()
		cart.Add(99)
	}()

	go func() {
		defer wg.Done()
		cart.Add(71)
	}()

	wg.Wait()
	fmt.Printf("total=%d\n", cart.Total)
}

说明:这里的共享数据不是单个计数器,而是结构体里的字段,所以更适合用 sync.Mutex 保护临界区。

一般

第 4 题:实现线程安全的词频统计器

第 4 题 一般

编写一个 Go 程序,完成下面要求:

  • 定义结构体 WordCounter
  • 字段包括: mu sync.Mutex data map[string]int
  • 给它绑定两个方法: Add(word string) Get(word string) int
  • 准备下面这个切片:
1
words := []string{"go", "go", "map", "mutex", "go", "mutex"}
  • 为每个单词启动一个 goroutine,调用 Add
  • 等全部 goroutine 完成后,按下面顺序输出统计结果:
1
2
3
go: 3
map: 1
mutex: 2
查看参考答案
 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
package main

import (
	"fmt"
	"sync"
)

type WordCounter struct {
	mu   sync.Mutex
	data map[string]int
}

func (w *WordCounter) Add(word string) {
	w.mu.Lock()
	defer w.mu.Unlock()

	w.data[word]++
}

func (w *WordCounter) Get(word string) int {
	w.mu.Lock()
	defer w.mu.Unlock()

	return w.data[word]
}

func main() {
	words := []string{"go", "go", "map", "mutex", "go", "mutex"}

	counter := &WordCounter{
		data: make(map[string]int),
	}

	var wg sync.WaitGroup
	for _, word := range words {
		wg.Add(1)
		go func(word string) {
			defer wg.Done()
			counter.Add(word)
		}(word)
	}

	wg.Wait()

	order := []string{"go", "map", "mutex"}
	for _, word := range order {
		fmt.Printf("%s: %d\n", word, counter.Get(word))
	}
}

说明:这道题除了练习加锁,还顺手复习了结构体、方法、切片遍历和 map 计数。

第 5 题:使用 sync.Map 保存课程字符数

第 5 题 一般

编写一个 Go 程序,完成下面要求:

  • 准备切片:
1
lessons := []string{"变量", "切片", "接口"}
  • 为每个课程名启动一个 goroutine
  • 每个 goroutine 把“课程名 -> 字符数”存进 sync.Map
  • 等所有 goroutine 完成后:
    1. 使用 Load("接口") 输出
1
接口 chars=2
  1. 删除 "切片"
  2. 再次读取 "切片",如果不存在,输出
1
切片 已删除
  1. 使用 Range 统计剩余课程名的总字符数,并输出
1
totalChars=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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import (
	"fmt"
	"sync"
)

func main() {
	lessons := []string{"变量", "切片", "接口"}

	var lessonMap sync.Map
	var wg sync.WaitGroup

	for _, lesson := range lessons {
		wg.Add(1)
		go func(lesson string) {
			defer wg.Done()
			lessonMap.Store(lesson, len([]rune(lesson)))
		}(lesson)
	}

	wg.Wait()

	value, ok := lessonMap.Load("接口")
	if ok {
		chars := value.(int)
		fmt.Printf("接口 chars=%d\n", chars)
	}

	lessonMap.Delete("切片")

	_, ok = lessonMap.Load("切片")
	if !ok {
		fmt.Println("切片 已删除")
	}

	totalChars := 0
	lessonMap.Range(func(key, value any) bool {
		chars, ok := value.(int)
		if ok {
			totalChars += chars
		}
		return true
	})

	fmt.Printf("totalChars=%d\n", totalChars)
}

说明:

  • Load 取出的值类型是 any,这里需要断言成 int 才能继续使用。
  • 这道题顺手复习了前面字符串章节里的 []rune,因为题目要求按“字符数”而不是字节数来统计。

第 6 题:修正并发切片追加程序

第 6 题 一般

下面程序的目标是:

  • 每个 goroutine 往 logs 里追加一条日志
  • 最后输出日志总数 count=3

但它现在有线程安全问题。请改正后让它正常运行。

 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
package main

import (
	"fmt"
	"sync"
)

func main() {
	names := []string{"阿斌", "小李", "小王"}
	logs := []string{}

	var wg sync.WaitGroup

	for _, name := range names {
		wg.Add(1)
		go func(name string) {
			defer wg.Done()
			logs = append(logs, name+" 完成了线程安全练习")
		}(name)
	}

	wg.Wait()

	fmt.Printf("count=%d\n", len(logs))
	for _, log := range logs {
		fmt.Println(log)
	}
}
查看参考答案

修正后的代码如下:

 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
package main

import (
	"fmt"
	"sync"
)

func main() {
	names := []string{"阿斌", "小李", "小王"}
	logs := []string{}

	var mu sync.Mutex
	var wg sync.WaitGroup

	for _, name := range names {
		wg.Add(1)
		go func(name string) {
			defer wg.Done()

			mu.Lock()
			logs = append(logs, name+" 完成了线程安全练习")
			mu.Unlock()
		}(name)
	}

	wg.Wait()

	fmt.Printf("count=%d\n", len(logs))
	for _, log := range logs {
		fmt.Println(log)
	}
}

说明:

  • append 操作可能触发切片扩容和底层数组变更,所以并发追加也需要同步保护。
  • 最后三条日志的先后顺序不固定,只要总数是 3 且每条日志都成功追加即可。

进阶

下面两题会把前面学过的结构体、方法、切片、map、WaitGroup、channel 和 select 一起串起来,但真正要解决的问题仍然是共享数据的线程安全。

第 7 题:实现并发订单统计中心

第 7 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义结构体 Order,字段包括: User string Amount int
  • 定义结构体 OrderCenter,字段包括: mu sync.Mutex totals map[string]int processed int64
  • OrderCenter 绑定方法 Record(order Order)
  • Record 中:
    1. Mutex 安全更新每个用户的订单总金额
    2. 用原子操作安全统计已处理订单数
  • 准备下面这组订单:
1
2
3
4
5
6
orders := []Order{
	{User: "阿斌", Amount: 120},
	{User: "小李", Amount: 80},
	{User: "阿斌", Amount: 30},
	{User: "小王", Amount: 50},
}
  • 为每笔订单启动一个 goroutine 调用 Record
  • 等全部处理完成后,输出:
1
2
3
4
processed=4
阿斌: 150
小李: 80
小王: 50
查看参考答案
 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
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

type Order struct {
	User   string
	Amount int
}

type OrderCenter struct {
	mu        sync.Mutex
	totals    map[string]int
	processed int64
}

func (c *OrderCenter) Record(order Order) {
	c.mu.Lock()
	c.totals[order.User] += order.Amount
	c.mu.Unlock()

	atomic.AddInt64(&c.processed, 1)
}

func main() {
	orders := []Order{
		{User: "阿斌", Amount: 120},
		{User: "小李", Amount: 80},
		{User: "阿斌", Amount: 30},
		{User: "小王", Amount: 50},
	}

	center := &OrderCenter{
		totals: make(map[string]int),
	}

	var wg sync.WaitGroup
	for _, order := range orders {
		wg.Add(1)
		go func(order Order) {
			defer wg.Done()
			center.Record(order)
		}(order)
	}

	wg.Wait()

	fmt.Printf("processed=%d\n", atomic.LoadInt64(&center.processed))

	orderUsers := []string{"阿斌", "小李", "小王"}
	for _, user := range orderUsers {
		fmt.Printf("%s: %d\n", user, center.totals[user])
	}
}

说明:这道题里两类共享数据的保护方式不同。

  • processed 是简单计数器,适合用原子操作。
  • totals 是共享 map,适合用互斥锁保护整段读写。

第 8 题:实现带超时的并发学习结果表

第 8 题 进阶

请完成一个稍微完整一点的小场景:

  • 定义结构体 LessonResult,字段包括: Name string Status string
  • 准备切片:
1
lessons := []string{"变量", "函数", "接口"}
  • 为每个课程启动一个 goroutine
  • 每个 goroutine 先模拟学习耗时,再把结果写入 sync.Map
  • sync.Map 的 key 使用课程名,value 使用 LessonResult
  • 另外准备一个 doneCh,每完成一个课程就发送一次完成信号
  • 主 goroutine 使用 select 同时等待:
    1. doneCh 的完成信号
    2. time.After(1 * time.Second) 的超时信号
  • 如果全部课程都完成,就按 lessons 的顺序输出:
1
2
3
变量 -> 已完成
函数 -> 已完成
接口 -> 已完成
  • 如果先超时,就输出:
1
timeout
查看参考答案
 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
package main

import (
	"fmt"
	"sync"
	"time"
)

type LessonResult struct {
	Name   string
	Status string
}

func main() {
	lessons := []string{"变量", "函数", "接口"}

	var results sync.Map
	doneCh := make(chan struct{}, len(lessons))
	timeout := time.After(1 * time.Second)

	for _, lesson := range lessons {
		go func(lesson string) {
			time.Sleep(50 * time.Millisecond)

			results.Store(lesson, LessonResult{
				Name:   lesson,
				Status: "已完成",
			})

			doneCh <- struct{}{}
		}(lesson)
	}

	completed := 0
	for completed < len(lessons) {
		select {
		case <-doneCh:
			completed++
		case <-timeout:
			fmt.Println("timeout")
			return
		}
	}

	for _, lesson := range lessons {
		value, ok := results.Load(lesson)
		if !ok {
			continue
		}

		result := value.(LessonResult)
		fmt.Printf("%s -> %s\n", result.Name, result.Status)
	}
}

说明:

  • 这里真正和本章强相关的是 sync.Map,因为多个 goroutine 会同时写入同一张结果表。
  • doneChselect 是前一章的知识,在这道题里只是自然地承担“完成通知”和“超时控制”的角色。
本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字