Goroutine 便宜,并不等于你可以随便 go func。泄漏的 goroutine 和泄漏的 setInterval 是同一类问题:没有人负责停下来。
select 把「等数据、等取消、等超时」写成一个分支表。
1func run(ctx context.Context, jobs <-chan Job) error {2 for {3 select {4 case <-ctx.Done():5 return ctx.Err()6 case job, ok := <-jobs:7 if !ok {8 return nil9 }10 if err := job.Do(ctx); err != nil {11 return err12 }13 }14 }15}ctx.Done() 优先退出,channel 关闭则收尾。没有 default 的 select 会阻塞,这通常是你想要的;加上 default 就变成轮询,CPU 会先抗议。
超时不要睡死
超时是取消的一种。用 context.WithTimeout,不要在业务里 time.Sleep 然后祈祷。
1func fetch(parent context.Context, url string) ([]byte, error) {2 ctx, cancel := context.WithTimeout(parent, 3*time.Second)3 defer cancel()4 5 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)6 if err != nil {7 return nil, err8 }9 res, err := http.DefaultClient.Do(req)10 if err != nil {11 return nil, err12 }13 defer res.Body.Close()14 return io.ReadAll(res.Body)15}只关闭你拥有的 channel
发送方关闭,接收方判断 ok。反过来关,是 panic 的经典现场。需要广播结束时,关的是 ctx,不是所有人的 channel。
1select {2case jobs <- job:3case <-ctx.Done():4 return ctx.Err()5}并发代码的可读性,取决于退出路径是否和进入路径一样显式。
Go 不靠类型系统拦住数据竞争(-race 是运行时的)。它靠的是:不要共享内存,用通信来共享。 select 就是那张通信日程表。Rust 用 所有权账本 在编译期做同样的事。取消信号从 HTTP 传到 worker,见 hono-typed-routes。
