协程和 channel
2026年9月6日
Goroutine 是运行时调度的轻量线程。前面加一个 go 就开起来。main 返回时,进程结束,不会等还在跑的 goroutine。
go func() {
fmt.Println("in goroutine")
}()
time.Sleep(time.Millisecond) // 只是演示;正式代码别靠 sleep 等
go
把 Sleep 去掉,多半什么都打印不出来:主函数先走完了。
协程
WaitGroup
要等一组 goroutine 结束,用 sync.WaitGroup:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
go
Add 在开 goroutine 之前调用。循环变量 i 要当参数传进去(Go 1.22 之前尤其必要)。
channel
goroutine 之间传数据用 channel,不要靠共享变量硬抢。
基本定义
ch := make(chan int) // 无缓冲,发送和接收必须同时准备好
buf := make(chan int, 3) // 有缓冲,没满之前发送不阻塞
go
只声明不 make 是 nil channel,收发都会永远卡住。
一边写、一边读:
ch := make(chan int)
go func() {
ch <- 1
ch <- 2
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
go
close 之后 range 会自己结束。往已关闭的 channel 发送会 panic;重复 close 也会 panic。接收已关闭的 channel 立刻拿到零值,用两个返回值区分:
v, ok := ch
if !ok {
fmt.Println("关了")
}
go
同步函数里自己发给自己、中间没有别的 goroutine,无缓冲 channel 会自己卡死。
select
多个 channel 都可能有数据时,用 select 等其中就绪的那个。多个同时就绪就随机挑一个。
select {
case v := <-ch1:
fmt.Println("ch1", v)
case v := <-ch2:
fmt.Println("ch2", v)
}
go
协程超时处理
time.After 也是 channel,和业务 channel 一起 select:
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(time.Second):
fmt.Println("超时")
}
go
default 分支表示「现在谁都没就绪就立刻走」,用来做非阻塞收发。
context.WithTimeout 更适合把超时往调用链下传,网络那一课会再用到。