上一篇主要整理了 Go 中函数、struct、指针和 interface 的基础用法。
这一篇继续记录 Go 中非常有特色的一部分:并发编程。
本篇主要整理目前已经学习过的几个知识点:
goroutine channel 缓冲 channel close WaitGroup defer Mutex select timeout这些内容以后在 Go 后端、网络编程以及并发任务处理中都会经常遇到。
一、goroutine 是什么
goroutine 可以先简单理解成:
让一个函数并发执行。
普通调用:
func work() { fmt.Println("working") } func main() { work() }这里work()会按照正常顺序执行。
如果加上:
go work()就表示:
启动一个新的 goroutine 来执行
work()。
例如:
package main import "fmt" func work() { fmt.Println("working") } func main() { go work() }需要注意:
如果main()太快结束,新的 goroutine 可能还没来得及执行,整个程序就已经退出了。
所以 goroutine 启动之后,通常还需要配合其他机制进行等待或同步。
二、channel 是什么
channel 可以理解成:
goroutine 之间传递数据的通道。
创建一个传递整数的 channel:
ch := make(chan int)发送数据:
ch <- 10接收数据:
x := <-ch例如:
package main import "fmt" func main() { ch := make(chan int) go func() { ch <- 20 }() x := <-ch fmt.Println(x) }输出:
20这里:
ch <- 20表示把20放进 channel。
而:
x := <-ch表示从 channel 取出一个值。
三、channel 还可以用于等待
如果:
x := <-ch执行时 channel 里暂时没有数据,那么当前 goroutine 会在这里等待。
直到另一个 goroutine执行:
ch <- 100程序才会继续。
所以 channel 不仅能传递数据,还能起到一定的同步作用。
四、无缓冲 channel
下面这种:
ch := make(chan int)叫做无缓冲 channel。
它的特点是:
发送和接收通常需要配合进行。
例如:
ch := make(chan int) ch <- 10如果没有其他 goroutine 来接收数据,那么:
ch <- 10可能会一直阻塞。
正确示例:
ch := make(chan int) go func() { ch <- 10 }() fmt.Println(<-ch)输出:
10五、有缓冲 channel
还可以创建有缓冲的 channel:
ch := make(chan int, 2)这里的:
2表示 channel 最多可以暂时存放 2 个整数。
例如:
ch := make(chan int, 2) ch <- 10 ch <- 20 fmt.Println(<-ch) fmt.Println(<-ch)输出:
10 20可以先理解为:
[10, 20]先放进去的值先被取出来。
如果缓冲区已经满了,再继续发送数据,就会阻塞,直到有人先取走数据。
六、关闭 channel
可以使用:
close(ch)关闭 channel。
需要注意:
关闭 channel 并不会立即清空里面已经存在的数据。
例如:
ch := make(chan int, 2) ch <- 10 ch <- 20 close(ch) fmt.Println(<-ch) fmt.Println(<-ch)仍然可以得到:
10 20close(ch)更准确的意思是:
告诉接收方,以后不会再发送新的数据了。
七、判断 channel 是否已经读取完
可以这样接收:
value, ok := <-ch其中:
value -> 读到的数据 ok -> 是否还能正常读取例如:
ch := make(chan int, 1) ch <- 8 close(ch) value, ok := <-ch此时:
value = 8 ok = true如果数据已经全部读取完,再读取一次:
value = 0 ok = false八、使用 range 读取 channel
channel 也可以配合range:
for value := range ch { fmt.Println(value) }例如:
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 close(ch) for value := range ch { fmt.Println(value) }输出:
1 2 3这个循环会一直读取,直到:
channel 已经关闭,并且里面的数据全部读取完。
所以如果使用:
for value := range ch一般要注意 channel 是否会被关闭,否则循环可能一直等待。
九、WaitGroup 是什么
如果启动了多个 goroutine,有时希望主程序等它们全部执行结束。
这时候可以使用:
sync.WaitGroup例如:
var wg sync.WaitGroup常见三个操作:
wg.Add(1) wg.Done() wg.Wait()可以理解为:
Add(n) -> 登记 n 个任务 Done() -> 当前任务完成 Wait() -> 等待所有任务完成例如:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println("任务1") wg.Done() }() go func() { fmt.Println("任务2") wg.Done() }() wg.Wait() fmt.Println("全部完成") }任务1和任务2的先后顺序不一定固定。
但:
全部完成一定会在两个任务都结束之后输出。
十、defer 的基础用法
defer可以先理解成:
把某个操作延迟到当前函数结束之前执行。
例如:
func test() { defer fmt.Println("B") fmt.Println("A") }输出:
A B因为:
defer fmt.Println("B")不会马上执行,而是在函数结束之前执行。
十一、多个 defer 的执行顺序
如果有多个defer:
func test() { defer fmt.Println("A") defer fmt.Println("B") defer fmt.Println("C") fmt.Println("D") }输出:
D C B A多个defer是:
后进先出。
也就是最后登记的defer最先执行。
十二、defer 和 WaitGroup 配合
实际代码里常写:
go func() { defer wg.Done() fmt.Println("working") }()这样可以保证当前 goroutine 结束时执行:
wg.Done()相比把wg.Done()手动放在函数最后,更不容易忘记。
十三、Mutex 是什么
多个 goroutine 同时修改同一个变量时,可能出现数据竞争。
例如:
count++看起来只有一行,但实际可以理解成:
读取 count 加 1 写回 count如果多个 goroutine 同时进行,就可能导致结果不准确。
这时候可以使用:
sync.Mutex也就是互斥锁。
基本写法:
mu.Lock() count++ mu.Unlock()可以理解为:
Lock() -> 我现在开始修改,其他 goroutine 先等一下 Unlock() -> 我修改完成了,其他 goroutine 可以继续十四、WaitGroup 和 Mutex 综合使用
例如启动 5 个 goroutine,每个都让count加 1:
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var mu sync.Mutex count := 0 for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() count++ mu.Unlock() }() } wg.Wait() fmt.Println(count) }最终输出:
5这里:
WaitGroup负责等待 5 个 goroutine 全部完成。
而:
Mutex负责保证count++不会被多个 goroutine 同时修改。
十五、goroutine 把结果通过 channel 返回
例如:
func square(n int, ch chan int) { ch <- n * n }调用:
ch := make(chan int) go square(5, ch) result := <-ch fmt.Println(result)输出:
25这里:
square()在新的 goroutine 中进行计算。
然后通过:
ch <- n * n把结果传回主 goroutine。
十六、并发任务完成顺序不固定
例如:
go square(2, ch) go square(3, ch) a := <-ch b := <-ch最终得到的两个结果一定是:
4 9但:
a = 4, b = 9和:
a = 9, b = 4都有可能。
原因是:
goroutine 的启动顺序,不代表它们一定按照这个顺序完成。
这是学习并发时比较重要的一点。
十七、select 是什么
select用来处理多个 channel。
例如:
select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) }可以理解为:
同时等待多个 channel,哪个先准备好,就先处理哪个。
如果ch1先有数据,就执行第一个case。
如果ch2先有数据,就执行第二个。
十八、select 中的 default
例如:
select { case value := <-ch: fmt.Println(value) default: fmt.Println("no data") }如果当前 channel 没有数据,那么不会一直等待,而是直接执行:
no data所以:
default可以理解为:
如果当前没有任何 case 可以立即执行,就执行 default。
需要注意:
default不是最后一定会执行。
只要有其他case可以执行,就不会进入default。
十九、select + timeout 超时控制
实际开发中,经常不能一直等待某个任务。
这时可以使用:
time.After()例如:
select { case msg := <-ch: fmt.Println(msg) case <-time.After(2 * time.Second): fmt.Println("timeout") }意思是:
等待 channel 的数据,但最多等待 2 秒。
如果 2 秒内收到数据,就处理数据。
如果 2 秒仍然没有收到,就输出:
timeout二十、超时控制完整例子
package main import ( "fmt" "time" ) func worker(ch chan string) { time.Sleep(1 * time.Second) ch <- "任务完成" } func main() { ch := make(chan string) go worker(ch) select { case msg := <-ch: fmt.Println(msg) case <-time.After(2 * time.Second): fmt.Println("timeout") } }因为任务只需要等待 1 秒,而超时时间是 2 秒,所以最终输出:
任务完成如果把:
time.Sleep(1 * time.Second)改成:
time.Sleep(3 * time.Second)那么超时时间会先到,最终输出:
timeout二十一、目前容易混淆的几个地方
1. goroutine 和 channel 不是同一个东西
goroutine 是:
执行任务的并发单元。
channel 是:
goroutine 之间传递数据的通道。
可以简单理解成:
goroutine -> 干活的人 channel -> 传递数据的通道2. goroutine 不是进程
一开始容易把:
go func()理解成“创建新进程”。
实际上更准确的说法是:
启动新的 goroutine。
goroutine 是 Go 运行时管理的轻量级并发任务,并不是操作系统进程。
3. 启动顺序不等于完成顺序
例如:
go task1() go task2()并不能保证:
task1 一定先完成 task2 一定后完成这也是并发程序需要特别注意的地方。
4. WaitGroup 和 Mutex 作用不同
WaitGroup:
等待多个 goroutine 全部完成。
Mutex:
防止多个 goroutine 同时修改共享数据。
它们解决的是两个不同的问题。
二十二、学习总结
这一阶段主要学习了 Go 的并发基础。
目前我对几个核心概念的理解是:
goroutine -> 并发执行任务channel -> goroutine 之间传递数据WaitGroup -> 等待多个 goroutine 完成Mutex -> 保护共享数据,避免数据竞争defer -> 函数结束前执行指定操作select -> 同时处理多个 channeltime.After -> 实现简单的超时控制Go 的并发部分比前面的基础语法更抽象,目前还只是掌握了基本概念和简单用法。
后面还需要通过实际项目继续巩固。
下一阶段准备开始学习 Go 的工程基础,包括:
package go mod 项目目录结构 JSON 文件读写 go test之后再进入 HTTP、Gin 和 MySQL,开始真正编写 Go 后端项目。