面试题答案
一键面试不同特点
- 发送和接收操作的同步性:
- 无缓冲通道:发送操作(
<-
)和接收操作(<-
)是同步的。也就是说,当一个 goroutine 向无缓冲通道发送数据时,它会阻塞,直到另一个 goroutine 从该通道接收数据;反之,当一个 goroutine 尝试从无缓冲通道接收数据时,它也会阻塞,直到有另一个 goroutine 向该通道发送数据。这就像是两个人传递物品,必须一个人给,另一个人同时接,双方都要等待对方准备好。 - 带缓冲通道:发送操作在通道缓冲区未满时不会阻塞,接收操作在通道缓冲区不为空时不会阻塞。通道缓冲区可以看作是一个小的“存储池”,只要这个“存储池”还有空间(对于发送操作)或者还有数据(对于接收操作),操作就可以继续进行,不需要立即有对应的接收者或发送者。例如,一个人可以先把物品放到一个小盒子(缓冲区)里,只要盒子没满,放的人就不用等别人来拿;同样,只要盒子里有东西,拿的人也不用等别人放进来。
- 无缓冲通道:发送操作(
- 初始化及容量:
- 无缓冲通道:初始化时不需要指定容量,例如
ch := make(chan int)
。 - 带缓冲通道:初始化时需要指定容量,例如
ch := make(chan int, 5)
,这里的5
就是通道的缓冲区大小,表示该通道可以容纳 5 个int
类型的数据。
- 无缓冲通道:初始化时不需要指定容量,例如
- 零值状态下操作:
- 无缓冲通道:零值为
nil
,向零值无缓冲通道发送数据或从其接收数据都会导致永久阻塞。 - 带缓冲通道:零值同样为
nil
,但行为和无缓冲通道类似,向零值带缓冲通道发送数据或从其接收数据也会导致永久阻塞,不过因为有缓冲区概念,其在非零值时的操作特性与无缓冲通道不同。
- 无缓冲通道:零值为
实际场景应用举例 - 任务队列
假设我们有一个 Web 服务器,会接收到大量的用户请求任务。我们可以使用带缓冲通道来构建一个任务队列,将这些请求任务暂时存储起来,然后由一定数量的工作 goroutine 从通道中取出任务并处理。这样可以防止短时间内大量请求直接压垮处理逻辑。
以下是一个简单的示例代码:
package main
import (
"fmt"
"time"
)
func worker(id int, tasks <-chan int) {
for task := range tasks {
fmt.Printf("Worker %d started task %d\n", id, task)
time.Sleep(time.Second) // 模拟任务处理时间
fmt.Printf("Worker %d finished task %d\n", id, task)
}
}
func main() {
// 创建一个带缓冲通道作为任务队列,缓冲区大小为10
tasks := make(chan int, 10)
// 启动3个工作 goroutine
for i := 1; i <= 3; i++ {
go worker(i, tasks)
}
// 模拟向任务队列添加任务
for i := 1; i <= 20; i++ {
tasks <- i
fmt.Printf("Added task %d to the queue\n", i)
}
// 关闭通道,告诉工作 goroutine 没有更多任务了
close(tasks)
// 等待一段时间,确保所有任务都被处理完
time.Sleep(5 * time.Second)
}
在这个示例中,tasks
是一个带缓冲通道,充当任务队列。worker
函数作为工作 goroutine 从通道中接收任务并处理。main
函数向通道中添加任务,由于通道有缓冲区,添加任务不会立即阻塞,直到缓冲区满。工作 goroutine 会从通道中取出任务并处理,实现了一种简单的任务队列机制。