先看最常见的场景:订单接口收到订单后,让后台任务生成账单。主流程和后台任务是两条 goroutine;它们不只要“都能运行”,还要解决订单交给谁、没交到时谁等待。
读写时机没有约定
后台任务可能在订单还没写完时就读取;主流程也不知道对方何时已经接手。还需要额外的锁和通知机制。
数据与等待规则一起表达
发送语句负责交出订单;接收语句负责取得订单。暂时无法完成时,运行时会让当前 goroutine 等待。
下面代码中,orders 是“订单交接通道”,orderID 是后台任务当前接到的订单编号。变量先有现实身份,再看箭头就不抽象了。
orders := make(chan int) go func() { orderID := <-orders fmt.Println("开始生成账单:", orderID) }() orders <- 1001
1001。如果接收方还没准备好,无缓冲 Channel 会让发送方等一等,而不是把订单悄悄丢掉。再把这段代码还原成基础语法
把 Channel 想成“goroutine 之间传东西的管道”。chan int 表示“这根管道里只能传 int”;cap(ch) 表示这个管道最多能暂存多少个值,也就是容量。
make(chan int)
没有存货位置,容量是 0。
ch := make(chan int) fmt.Println(cap(ch)) // 0
make(chan int, 3)
内部最多暂存 3 个 int。
ch := make(chan int, 3) fmt.Println(cap(ch)) // 3
三个必须认识的符号
| 语法 | 含义 | 读法 |
|---|---|---|
ch <- 10 | 向 Channel 发送 10 | 把 10 放进 ch |
x := <-ch | 从 Channel 接收一个值 | 从 ch 拿一个值给 x |
x, ok := <-ch | 接收值,同时判断是否还收到真正发送过的数据 | ok 为 false 表示 channel 已关闭且已经没有数据 |
<- 的箭头方向就是数据流方向。
ch <- v:数据流进 ch;v := <-ch:数据从 ch 流出来。
最小对照:为什么通常要配 goroutine?
只改变一个条件:第一段只有主 goroutine,第二段多一个负责发送的 goroutine。这样可以直接看出阻塞来自哪里。
orders := make(chan int) orders <- 1001 // 这里会一直等接收者 orderID := <-orders // 永远走不到这里
无缓冲 Channel 的发送需要有人同时接收。这里只有一个 goroutine,它自己卡在发送处,后面的接收语句永远执行不到,因此运行时最终会报死锁。
orders := make(chan int) go func() { orders <- 1001 }() orderID := <-orders fmt.Println(orderID)
go func() { ... }() 表示启动一个新的 goroutine。现在“发送”和“接收”可以由两个 goroutine 配对。