核心概念速览:
- goroutine → 轻量级并发执行单元
- channel → goroutine 之间传递数据
- select → 监听多个 channel
- WaitGroup → 等待一组 goroutine 完成
- Mutex → 保护共享数据
goroutine 轻量级并发执行单元
goroutine 是 GoLang 提供的一种用户级线程(协程)。Go Runtime 将用户编写的大量 goroutine 调度到少量的操作系统级别的线程上执行。
go
goroutine (G) -> Go Runtime 调度 -> OS Thread(线程) -> CPU
这就是 Go 最著名的 GMP 模型。
go
package main
import "fmt"
func test() {
fmt.Println("hello")
}
func main() {
// 普通的函数调用前加上 go 关键字,该函数就会被当作一个 goroutine 执行
go test()
}
不过以上的这个模型可能不会有任何输出。因为 main 函数退出,所有的 goroutine 都得退出,不管你执行到了哪里。
可以通过 time.Sleep() 方法,阻塞主进程,让 goroutine 能够有足够的时间执行完毕。
go
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("hello")
}
func main() {
go hello()
time.Sleep(time.Second)
}
WaitGroup
在上面使用了 time.Sleep() 方法来等待 goroutine 执行完毕,仅做演示用。在正式项目中正规的做法是使用 WaitGroup 机制等待 goroutine 执行完毕。
go
package main
import (
"fmt"
"sync"
)
func hello(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("hello")
}
func main() {
var wg sync.WaitGroup
wg.add(1)
go hello(&wg)
// 等待 hello 执行完毕
wg.wait()
}
启动多个 goroutine 并等待它们全部执行完毕。
go
package main
import (
"fmt"
"sync"
)
func worker(i int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("worker: " + i)
}
func main() {
var wg sync.WaitGroup
for i := 1; i < 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
}
通过匿名函数传递参数,容易出现闭包陷阱的问题。
(闭包捕获循环变量,很多编程语言都有的 bug, Go 1.22 修复了这个问题)
go
package main
import "fmt"
func main() {
// 正确方式
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
// 不推荐的方式
for j := 0; j < 5; j++ {
go func() {
fmt.Println(j)
}()
}
}
channel
channel 是一个带有同步能力的队列。
go
goroutine A -> channel -> goroutine B
例如:
go
package main
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch)
}
这个例子体现了 channel 天生就是自带同步功能的。这其中发生了两件事情,数据传递和同步。
数据传递:
go
42 -> channel -> main
同步:
go
<-ch 没有数据时,会阻塞 main
直到 goroutine ch <- 42,main 接收到了 goroutine 发送过来的 数据,才继续向下执行
带缓冲 channel
通过以下语法:
go
ch := make(chan int, 3)
可以创建一个带缓冲的 channel
发送方可以连续发送:
go
ch <- 1
ch <- 2
ch <- 3
不会阻塞。
发送了第四个
go
ch <- 4 时,发送方才会阻塞
所以带缓冲的 channel 可以当作异步通信,而没带缓冲的 channel 可以被当作同步通信。
这是这两类 channel 之间的区别。