不加 default 的 select 会永久阻塞,因它必须等待至少一个 case 就绪;若所有 channel 均不可读写且无 default,则 goroutine 陷入死锁。

为什么 select 里不加 default 可能导致 goroutine 永久阻塞
Go 的 channel 是带缓冲或无缓冲的通信管道,但它的阻塞行为常被误判。比如向一个无缓冲 channel 发送数据时,若没有 goroutine 同时在另一端接收,send 操作会一直挂起——这本身是设计使然,但容易在逻辑分支中被忽略。
常见错误场景:多个 channel 等待响应,但没考虑“所有 channel 都暂不可用”的情况。
- 无缓冲 channel 的
send和recv必须成对出现,否则至少一方永久等待 - 有缓冲 channel(如
make(chan int, 1))可暂存一个值,但缓冲满后仍会阻塞send -
select中不写default,就等同于“必须等到某个 case 就绪”,没有兜底逻辑
ch := make(chan int) go func() { time.Sleep(2 * time.Second) ch <- 42>如何安全关闭 channel 并避免 panic: send on closed channel
channel 关闭后不能再发送,但可以继续接收(已缓存的数据 + 零值)。错误常出现在多生产者场景下:谁该关?何时关?关早了其他 goroutine 还在发,就 panic。
原则:** 只由发送方关闭,且确保所有发送操作已完成 **。推荐用 sync.WaitGroup 协调。
立即学习 “go 语言免费学习笔记(深入)”;
- 从已关闭 channel 接收不会 panic,返回零值 +
false(如 v, ok := 中 ok==false) - 重复关闭 channel 会 panic,所以不要在多个 goroutine 里都写
close(ch) - 如果用
range 遍历 channel,它会在 channel 关闭且缓冲为空时自动退出
ch := make(chan int, 2) go func() { defer close(ch) // 正确:由 sender defer 关闭 ch <- 1 ch <- 2}() for v := range {>chan 和 类型的区别直接影响函数参数设计
Go 的 channel 类型支持方向限定:chan 表示“只能发送”, 表示“只能接收”。这不是语法糖,而是编译期检查机制,用错会报错。
典型用途:限制函数职责,防止意外写入或读取,提升接口安全性。
- 函数参数声明为
,调用方传入普通 chan int 或 都合法(协变) - 但不能把
传给期望 chan 的参数(方向不匹配) - 返回
chan int 的函数,若只想暴露接收能力,应返回
func counter(out chan<- int) {>func printer(in <-chan int) {>
用 time.After 配合 select 实现超时控制,但别直接传给多个 goroutine
time.After 返回一个单次触发的 ,常用于超时判断。但它不是“可重用资源”——每次调用才新建 channel,重复使用旧的 channel 会导致超时逻辑失效。
错误模式:把同一个 time.After(1*time.Second) 结果传给多个 select,第一个触发后 channel 就已读空,后续 select 永远等不到它。
- 每个需要独立超时判断的
select块,应调用一次time.After - 若需复用超时逻辑,可用
time.NewTimer并手动Reset,但注意并发安全 - 更轻量做法:在循环内每次重新生成
time.After,语义清晰且无状态管理负担
ch := make(chan string, 1) go func() { time.Sleep(1500 * time.Millisecond) ch <- "done"}() select {case msg := <-ch: fmt.println("received:", msg) case <-time.after(1 * time.second):>
实际写并发逻辑时,最易被忽略的是 channel 的生命周期归属和方向控制——不是“能通就行”,而是“谁建、谁关、谁读、谁写”必须提前约定清楚。类型系统能拦住一部分错误,但关 channel 的时机、select 的兜底、超时 channel 的复用,这些得靠结构设计来保障。