Go语言中调用context cancel()后等待goroutine完成的最佳实践
最佳实践:用退出通道同步goroutine完成
核心思路是新增一个done通道,专门用来等待run goroutine执行完毕,替代不可靠的time.Sleep或重量级的WaitGroup(毕竟只有单个goroutine)。
修改后的代码实现
main包代码
func main() { log.Infoln("starting service..") ctx := context.Background() srv := service.Service{} exitCh := make(chan os.Signal, 1) signal.Notify(exitCh, syscall.SIGTERM, // terminate: 对应`kill PID`(注意:kill -9是强制杀死,无法被程序捕获) syscall.SIGINT, // interrupt: 对应Ctrl + C ) ctxCancel, cancel := context.WithCancel(ctx) done := make(chan struct{}) // 新增:用于等待goroutine退出的同步通道 go run(ctxCancel, srv, done) <-exitCh // 阻塞直到收到系统退出信号 cancel() // 触发context取消,让service.Start()进入退出逻辑 <-done // 等待goroutine完全执行完毕后,再退出main函数 } func run(ctx context.Context, srv service.Service, done chan<- struct{}) { defer close(done) // 函数退出时关闭done通道,通知main已完成所有逻辑 err := srv.Start(ctx) if err != nil { log.Warningf("canceling all operations: %s", err) } }
service包代码(无需修改)
type Service struct {} func (s *Service) Start(ctx context.Context) error { for i := 0; i < 5; i++ { select { case <-ctx.Done(): log.Println("Done") return ctx.Err() default: log.Printf("i value: %d", i) time.Sleep(3 * time.Second) } } return nil }
方案说明
- 新增的
done通道是无缓冲的struct{}类型,仅用于同步单个goroutine的退出状态,比WaitGroup更简洁,不需要初始化、计数、等待那套流程。 run函数中用defer close(done),确保不管Start函数是正常返回还是因context取消返回,都会触发done通道的关闭,main里的<-done会被唤醒,保证main不会提前退出。- 移除了原
run函数中向exitCh发送信号的逻辑——这部分是多余的,因为main已经在监听系统信号,run的退出不需要再向该通道发信号,避免潜在的通道死锁。
内容的提问来源于stack exchange,提问作者Soc
相关产品推荐
相关产品推荐

