有缓冲通道
容量、关闭通道与使用 range 遍历通道
有缓冲通道 是 CoddyKit 上的免费 Go Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Go Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Go Academy 课程共包含 4 节课。
什么是有缓冲通道
有缓冲通道具有容量,只要缓冲区有空间,发送就不会阻塞。通道的行为类似队列:
package main
import "fmt"
func main() {
ch := make(chan int, 3) // capacity 3
ch <- 1 // does not block — buffer has room
ch <- 2 // does not block
ch <- 3 // does not block
// ch <- 4 // would block — buffer full
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
}将有缓冲通道用作队列
有缓冲通道可以在 goroutine 之间实现 FIFO 队列:
package main
import ("fmt"; "sync")
func producer(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 5; i++ {
ch <- i
fmt.Printf("produced %d\n", i)
}
}
func main() {
ch := make(chan int, 2) // buffer 2 — producer runs ahead
var wg sync.WaitGroup
wg.Add(1)
go producer(ch, &wg)
wg.Wait()
close(ch)
for v := range ch { fmt.Printf("consumed %d\n", v) }
}通道上的 len() 和 cap()
检查有缓冲通道当前的大小和容量:
package main
import "fmt"
func main() {
ch := make(chan string, 5)
ch <- "a"
ch <- "b"
ch <- "c"
fmt.Println("cap:", cap(ch)) // 5
fmt.Println("len:", len(ch)) // 3 — items currently buffered
<-ch
fmt.Println("len:", len(ch)) // 2
}解耦 producer 和 consumer
缓冲区可以解耦 producer 与 consumer 的速度。在阻塞之前,producer 可以领先 consumer 的程度取决于缓冲区大小:
package main
import ("fmt"; "sync"; "time")
func produce(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
}
}
func consume(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for v := range ch {
time.Sleep(10 * time.Millisecond) // slower consumer
fmt.Println("consumed:", v)
}
}
func main() {
ch := make(chan int, 3)
var wg sync.WaitGroup
wg.Add(2)
go produce(ch, &wg)
go consume(ch, &wg) // BUG: produce closes before consume sees all
// In practice, coordinate close carefully
fmt.Println("decoupled")
}使用有缓冲通道实现信号量
容量为 N 的有缓冲通道可以将并发 goroutine 的数量限制为 N,这是一种简单的信号量:
package main
import ("fmt"; "sync"; "time")
func main() {
sem := make(chan struct{}, 3) // max 3 concurrent
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
sem <- struct{}{} // acquire
defer func() { <-sem }() // release
fmt.Printf("job %d running\n", i)
time.Sleep(50 * time.Millisecond)
}()
}
wg.Wait()
}关闭有缓冲通道
关闭有缓冲通道后,接收方仍可以取出缓冲区中剩余的值,然后才检测到通道已关闭:
package main
import "fmt"
func main() {
ch := make(chan int, 5)
for i := 1; i <= 3; i++ { ch <- i }
close(ch)
// Drain all buffered values after close:
for v := range ch {
fmt.Println(v) // 1, 2, 3
}
// range exits after channel closed and drained
}有缓冲与无缓冲:如何选择
选择指南:
- 无缓冲:有保障的同步交接、严格顺序以及会合式同步
- 有缓冲:解耦 producer 和 consumer 的速度、减少阻塞、实现信号量以及收集 N 个结果
- 默认使用无缓冲通道,只有在能够说明缓冲区大小合理时才添加缓冲
- 缓冲区太小 → 仍会阻塞;太大 → 会掩盖错误
使用有缓冲通道收集结果
使用有缓冲通道收集并行 goroutine 的结果,避免发生阻塞:
package main
import ("fmt"; "sync")
func square(n int) int { return n * n }
func main() {
nums := []int{1, 2, 3, 4, 5}
results := make(chan int, len(nums)) // buffer = goroutine count
var wg sync.WaitGroup
for _, n := range nums {
wg.Add(1)
n := n
go func() {
defer wg.Done()
results <- square(n)
}()
}
wg.Wait()
close(results)
for r := range results { fmt.Println(r) }
}缓冲区已满导致的 goroutine 泄漏
如果缓冲区已满且没有读取方,发送方就会永久阻塞,从而导致 goroutine 泄漏:
package main
import ("fmt"; "time"; "runtime")
func main() {
ch := make(chan int, 2)
// Start goroutine that fills buffer and then blocks
go func() {
ch <- 1
ch <- 2
ch <- 3 // blocks: buffer full, no receiver
fmt.Println("never reached")
}()
time.Sleep(50 * time.Millisecond)
fmt.Println("goroutines:", runtime.NumGoroutine()) // 2 — leaked
}将 time.After 用作有缓冲通道
time.After返回一个容量为 1 的<-chan time.Time。运行时会在指定持续时间后向其中发送数据,随后该通道会被垃圾回收:
package main
import ("fmt"; "time")
func doWithTimeout(work func() string) (string, bool) {
result := make(chan string, 1)
go func() { result <- work() }()
select {
case r := <-result:
return r, true
case <-time.After(100 * time.Millisecond):
return "", false
}
}
func main() {
r, ok := doWithTimeout(func() string {
time.Sleep(50 * time.Millisecond)
return "done"
})
fmt.Println(r, ok)
}快速检查
向容量已满的有缓冲通道发送数据时会发生什么?
回顾:有缓冲通道
摘要:
make(chan T, N)创建容量为 N 的有缓冲通道- 仅当缓冲区已满时发送才会阻塞;仅当缓冲区为空时接收才会阻塞
len(ch)——当前项目数;cap(ch)——容量- 用于解耦 producer 和 consumer、收集并行结果以及实现信号量
- 关闭通道后可以先取出剩余数据,再到达 EOF;
range ch会自动处理这一过程
常见问题解答
「有缓冲通道」课时是免费的吗?
是的 — 「有缓冲通道」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Go Academy 课程的其余内容,请升级到 CoddyKit PRO。 Go Academy 课程共包含 4 节课。
「有缓冲通道」这节课中我会学到什么?
容量、关闭通道与使用 range 遍历通道 你通过在浏览器中直接运行的动手代码来练习 Go Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Go Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Go Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「有缓冲通道」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Go Academy 课中编写并运行代码吗?
能。每节 Go Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。