一、并发 vs 并行
在学习 goroutine 之前,先分清两个概念:
| 概念 | 说明 | 比喻 |
|---|---|---|
| 并发(Concurrency) | 同时处理多个任务 | 一个人同时泡面+刷手机,一会做这个一会做那个 |
| 并行(Parallelism) | 同时执行多个任务 | 两个人,一个泡面一个刷手机 |
Go 的 goroutine 让编写并发代码变得极其简单——它不保证并行(取决于 CPU 核数),但让你轻松写出并发逻辑。
二、goroutine(协程)
2.1 基本用法
在函数调用前面加一个 go 关键字,这个函数就会在 goroutine 中并发执行:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
go printNumbers() // 并发执行
fmt.Println("main 继续执行...")
time.Sleep(3 * time.Second) // 等 goroutine 跑完
}
2.2 goroutine 的特点
| 特性 | 说明 |
|---|---|
| 轻量级 | 初始栈只有几 KB,可以轻松启动上万个 |
| 非阻塞 | 启动 goroutine 后,程序继续往下执行 |
| 不等待 | main 函数退出时,所有 goroutine 都会被强制结束 |
⚠️ main 函数不会等待 goroutine。main 结束了,所有 goroutine 都会停止。所以上面的例子用了
time.Sleep来等。
三、sync.WaitGroup(等待组)
用 time.Sleep 来等 goroutine 是不靠谱的。正确的方式是用 sync.WaitGroup:
import (
"fmt"
"sync"
"time"
)
func downloadFile(name string) {
fmt.Printf("下载中: %s\n", name)
time.Sleep(2 * time.Second)
fmt.Printf("完成: %s\n", name)
}
func main() {
var wg sync.WaitGroup
files := []string{"photo.jpg", "doc.pdf", "music.mp3"}
for _, f := range files {
wg.Add(1) // 计数器 +1
go func(filename string) {
defer wg.Done() // 计数器 -1(defer 确保函数结束时执行)
downloadFile(filename)
}(f)
}
wg.Wait() // 阻塞,直到计数器归零
fmt.Println("全部下载完成!")
}
WaitGroup 的三个方法
| 方法 | 作用 |
|---|---|
wg.Add(n) | 计数器 +n |
wg.Done() | 计数器 -1 |
wg.Wait() | 阻塞等待计数器归零 |
💡
defer关键字:函数返回前一定会执行的操作。defer wg.Done()确保不管函数是否出错,计数器都会减一。
四、channel(通道)
channel 是 goroutine 之间通信的管道。一个 goroutine 发送数据到 channel,另一个 goroutine 从 channel 接收数据。
4.1 基本语法
// 创建 channel
ch := make(chan int) // 无缓冲 channel,传输 int 类型
ch := make(chan string) // 传输 string 类型
// 发送数据
ch <- 42 // 箭头指向 channel = 发送
// 接收数据
value := <-ch // 箭头从 channel 出来 = 接收
4.2 生产者-消费者模式
func producer(out chan<- string) {
for i := 1; i <= 5; i++ {
out <- fmt.Sprintf("消息 #%d", i)
}
close(out) // 发送完毕,关闭 channel
}
func consumer(in <-chan string) {
for msg := range in { // 用 range 持续接收
fmt.Println("收到:", msg)
}
fmt.Println("处理完毕")
}
func main() {
ch := make(chan string)
go producer(ch)
consumer(ch)
}
4.3 channel 方向
函数参数可以指定 channel 方向,让代码更安全:
| 声明 | 方向 | 作用 |
|---|---|---|
out chan<- string | 只写 | 只能发送,不能接收 |
in <-chan string | 只读 | 只能接收,不能发送 |
ch chan string | 读写 | 双向 |
// 如果尝试在只写 channel 里接收,编译不通过
// val := <-out // ❌ 编译错误
五、无缓冲 vs 有缓冲
5.1 无缓冲 channel
- 发和收必须同时准备好,否则阻塞
- 保证发送和接收"同时发生"(同步)
ch := make(chan int) // 无缓冲
// 发送方会阻塞,直到有人接收
go func() {
ch <- 42 // 阻塞,直到 main 中的 <-ch 执行
}()
val := <-ch // 接收,同时唤醒发送方
5.2 有缓冲 channel
- 可以存 N 个值,存满才阻塞
- 发和收解耦(异步)
queue := make(chan string, 3) // 容量 3
queue <- "A" // 不阻塞
queue <- "B" // 不阻塞
queue <- "C" // 不阻塞,缓冲区满了
// queue <- "D" // 阻塞!缓冲区满了,需要有人取走
| 类型 | 容量 | 行为 |
|---|---|---|
make(chan T) | 0 | 无缓冲,必须收发同时进行 |
make(chan T, N) | N | 有缓冲,存满才阻塞 |
5.3 关闭 channel
close(ch) // 生产者发送完数据后关闭
// 接收方用 range 自动检测关闭
for msg := range ch {
fmt.Println(msg)
}
// 或者手动检测
msg, ok := <-ch
if !ok {
fmt.Println("channel 已关闭")
}
💡 只应由发送方关闭 channel。向已关闭的 channel 发送数据会 panic。
六、select(多路复用)
select 可以同时等待多个 channel,哪个 channel 有数据就处理哪个:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// 两个 goroutine 在不同时间返回
go func() {
time.Sleep(1 * time.Second)
ch1 <- "快递 A 到了"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "快递 B 到了"
}()
// 同时等待两个 channel
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg) // 1 秒后先到
case msg := <-ch2:
fmt.Println(msg) // 2 秒后到
case <-time.After(3 * time.Second):
fmt.Println("超时") // 防止永远阻塞
}
}
}
select 的特点
| 特性 | 说明 |
|---|---|
| 同时等待 | 多个 case 同时等待,谁先 ready 执行谁 |
| 随机公平 | 如果多个 channel 同时 ready,随机选一个执行 |
| 超时机制 | time.After 可以设置超时 |
| default | 没有 channel ready 时执行 default(非阻塞) |
七、完整示例:并发下载管理器
package main
import (
"fmt"
"sync"
"time"
)
type Result struct {
Name string
Size int
}
func fetch(name string) Result {
// 模拟不同大小的文件耗时不同
time.Sleep(time.Duration(len(name) * 100) * time.Millisecond)
return Result{Name: name, Size: len(name) * 1024}
}
func main() {
urls := []string{"photo.jpg", "doc.pdf", "music.mp3", "video.mp4"}
resultCh := make(chan Result, len(urls))
var wg sync.WaitGroup
// 并发获取所有文件
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resultCh <- fetch(u)
}(url)
}
// 等待所有下载完成,关闭 channel
go func() {
wg.Wait()
close(resultCh)
}()
// 读取结果(按完成顺序输出)
for r := range resultCh {
fmt.Printf("📄 %s (%d KB)\n", r.Name, r.Size)
}
}
输出:
📄 doc.pdf (7168 KB)
📄 music.mp3 (9216 KB)
📄 video.mp4 (9216 KB)
📄 photo.jpg (9216 KB)
注意:输出顺序不固定,每次运行可能不同——哪个文件先下载完就先显示哪个。
八、使用建议
8.1 什么时候用 goroutine
- 需要并发的 I/O 操作(网络请求、文件读写、数据库查询)
- 多个独立任务(不影响彼此结果)
- 后台任务(定时清理、日志写入)
8.2 注意事项
// ❌ 错误:循环变量被共享
for _, url := range urls {
go func() {
fetch(url) // url 可能已经被循环改变了!
}()
}
// ✅ 正确:通过参数复制
for _, url := range urls {
go func(u string) {
fetch(u)
}(url)
}
8.3 Go 的并发哲学
"Don't communicate by sharing memory; share memory by communicating." (不要通过共享内存来通信,要通过通信来共享内存。)
意思是:不要用锁去保护共享变量,而是用 channel 把数据从一个 goroutine 传给另一个。数据归谁,谁就操作它。
| 场景 | 推荐方案 |
|---|---|
| goroutine 间通信 | channel |
| 保护共享变量 | sync.Mutex |
| 等待多个 goroutine | sync.WaitGroup |
| 只执行一次 | sync.Once |
九、小结
| 知识点 | 一句话 |
|---|---|
| goroutine | go fn() 启动轻量级并发执行 |
| WaitGroup | Add/Done/Wait 等待一组 goroutine |
| channel | ch <- v 发送,v := <-ch 接收 |
| 带缓冲 channel | make(chan T, N) 解耦收发 |
| close | 发送方关闭 channel,接收方用 range 读 |
| select | 同时等待多个 channel,谁先来先处理谁 |
| defer | defer wg.Done() 确保函数结束时执行 |
| 并发哲学 | 用通信共享内存,不用共享内存通信 |