协程和 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

只声明不 makenil 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 更适合把超时往调用链下传,网络那一课会再用到。


参考文档