0Pricing
Go Academy · 课时

有缓冲通道

容量、关闭通道与使用 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 启动 Goroutine
  2. 无缓冲通道
  3. 有缓冲通道
  4. 通道方向与流水线模式
← 返回 Go Academy