上一篇主要整理了 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
20
close(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
-> 同时处理多个 channel
time.After
-> 实现简单的超时控制
Go 的并发部分比前面的基础语法更抽象,目前还只是掌握了基本概念和简单用法。
后面还需要通过实际项目继续巩固。
下一阶段准备开始学习 Go 的工程基础,包括:
package
go mod
项目目录结构
JSON
文件读写
go test
之后再进入 HTTP、Gin 和 MySQL,开始真正编写 Go 后端项目。




