Mgx曰:

“走得快,不如走得稳;退得急,不如退得净。”

在 Go 世界里,写一个能跑的程序很容易,但写一个能体面退出的程序,却是一门艺术。

今天,我们就从"Hello, World"级别的退出,一路打怪升级,直到搞定多级流水线清空退出这个终极 Boss!

🌱 一、最原始的退出:说走就走,不管员工死活

package main

import (
	"fmt"
	"time"
)

func main() {
	// 启动一个后台"打工人"
	go func() {
		for {
			fmt.Println("我在默默搬砖...")
			time.Sleep(1 * time.Second)
		}
	}()

	time.Sleep(3 * time.Second)
	fmt.Println("老板说下班了!")
	// 主程序直接退出,打工人被强制"蒸发"
}

输出:

我在默默搬砖...
我在默默搬砖...
我在默默搬砖...
老板说下班了!
(程序结束,后台 goroutine 被无情杀死)

问题:后台任务可能正在写文件、发请求、存数据库……你一走,他就"工伤"了!

✅ 二、初级优雅:用 channel 打个招呼

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan bool)

	go func() {
		for {
			select {
			case <-done:
				fmt.Println("收到下班通知,正在收拾桌面...")
				return // 体面退出
			default:
				fmt.Println("继续搬砖...")
				time.Sleep(1 * time.Second)
			}
		}
	}()

	time.Sleep(3 * time.Second)
	close(done) // 发送"下班"信号
	time.Sleep(1 * time.Second) // 等它收拾完
	fmt.Println("全员下班,关门!")
}

输出:

继续搬砖...
继续搬砖...
继续搬砖...
收到下班通知,正在收拾桌面...
全员下班,关门!

进步了!time.Sleep(1 * time.Second) 太随意——万一它收拾要 2 秒呢?

🧱 三、中级优雅:用 WaitGroup 等所有人下班

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	done := make(chan bool)

	// 招募 3 个打工人
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for {
				select {
				case <-done:
					fmt.Printf("打工人 %d:收到!正在关电脑...\n", id)
					return
				default:
					fmt.Printf("打工人 %d:搬砖中...\n", id)
					time.Sleep(800 * time.Millisecond)
				}
			}
		}(i)
	}

	time.Sleep(3 * time.Second)
	close(done)
	wg.Wait() // 耐心等所有人关电脑
	fmt.Println("办公室灯灭了,真优雅!")
}

输出:

打工人 1:搬砖中...
打工人 2:搬砖中...
打工人 3:搬砖中...
...
打工人 2:收到!正在关电脑...
打工人 1:收到!正在关电脑...
打工人 3:收到!正在关电脑...
办公室灯灭了,真优雅!

稳了! 但现实世界中,程序往往不是自己想退就退——用户会按 Ctrl+C,K8s 会发 SIGTERM!

📡 四、真实世界:监听系统信号(Ctrl+C 也不慌)

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// 创建一个信号接收器
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)

	done := make(chan bool)
	go func() {
		for {
			select {
			case <-done:
				fmt.Println("后台任务:收到指令,正在保存进度...")
				return
			default:
				fmt.Println("后台任务:运行中...")
				time.Sleep(1 * time.Second)
			}
		}
	}()

	fmt.Println("程序已启动,按 Ctrl+C 优雅退出")

	<-sigCh // 阻塞等待信号(比如 Ctrl+C)
	fmt.Println("\n检测到退出信号!准备体面告别...")

	close(done)
	time.Sleep(1 * time.Second) // 简单等待(后面会优化)
	fmt.Println("再见,世界!👋")
}

操作:

$ go run main.go
程序已启动,按 Ctrl+C 优雅退出
后台任务:运行中...
^C
检测到退出信号!准备体面告别...
后台任务:收到指令,正在保存进度...
再见,世界!👋

终于像生产环境了! 但 time.Sleep 还是不够专业……

🌟 五、Go 官方推荐:用 context 统一管理取消

context 是 Go 并发编程的"瑞士军刀",尤其适合传递取消信号。

package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func worker(ctx context.Context, id int) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("打工人 %d:收到取消指令,原因:%v,正在退出...\n", id, ctx.Err())
			return
		default:
			fmt.Printf("打工人 %d:努力工作中...\n", id)
			time.Sleep(1 * time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// 启动 3 个打工人
	for i := 1; i <= 3; i++ {
		go worker(ctx, i)
	}

	// 监听系统信号
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	<-sigCh

	fmt.Println("\n准备优雅退出...")
	cancel() // 通知所有打工人

	// 等待(实际项目中应结合 WaitGroup)
	time.Sleep(2 * time.Second)
	fmt.Println("全员安全撤离!")
}

标准做法! 但注意:context 的语义是"尽快取消",不保证处理完剩余任务!

🚨 六、高能预警:流水线中的"数据卡住"陷阱!

假设你有这样一个三级流水线:

Producer → [10个中间工人] → [3个最终消费者]

如果所有 goroutine 都监听 ctx.Done() 并立即退出,channel 里还没处理的数据就丢了!

💥 这就是"伪优雅退出"——表面体面,实则丢数据!

🏆 七、终极方案:两阶段退出 + 流水线排空

我们要做到:

  • 停止生产(源头切断)
  • 让流水线自然跑完(清空 channel)
  • 不丢一个数据
package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

func main() {
	// 两级缓冲通道
	ch1 := make(chan int, 100) // 存原始任务
	ch2 := make(chan int, 100) // 存处理结果

	var wg sync.WaitGroup

	// ========== 第一阶段:生产者(唯一响应退出信号)==========
	stopProducing := make(chan struct{})
	wg.Add(1)
	go func() {
		defer wg.Done()
		defer close(ch1) // 生产结束,关闭通道,通知下游"没新活了"

		for taskID := 1; taskID <= 50; taskID++ {
			select {
			case <-stopProducing:
				fmt.Println("生产者:收到停工指令,不再接新单!")
				return
			case ch1 <- taskID:
				fmt.Printf("生产者:发布任务 %d\n", taskID)
				time.Sleep(50 * time.Millisecond)
			}
		}
		fmt.Println("生产者:今日任务全部发布完毕!")
	}()

	// ========== 第二阶段:10个中间工人(不响应取消!只靠通道关闭退出)==========
	stage1Wg := &sync.WaitGroup{}
	stage1Wg.Add(10)
	for i := 1; i <= 10; i++ {
		go func(id int) {
			defer stage1Wg.Done()
			// 关键:这里 **不监听任何取消信号**!
			// 只要 ch1 没关,就一直干
			for task := range ch1 {
				result := task * 2
				fmt.Printf("中间工人 %d:处理任务 %d → 产出 %d\n", id, task, result)
				ch2 <- result
			}
			fmt.Printf("中间工人 %d:ch1 已关,下班!\n", id)
		}(i)
	}

	// 所有中间工人干完后,关闭 ch2
	go func() {
		stage1Wg.Wait()
		close(ch2)
		fmt.Println("所有中间工人下班,ch2 关闭!")
	}()

	// ========== 第三阶段:3个最终消费者 ==========
	wg.Add(3)
	for i := 1; i <= 3; i++ {
		go func(id int) {
			defer wg.Done()
			// 同样,只靠 range 自动退出
			for result := range ch2 {
				fmt.Printf("最终消费者 %d:收到成品 %d\n", id, result)
				time.Sleep(30 * time.Millisecond)
			}
			fmt.Printf("最终消费者 %d:无新货,收工!\n", id)
		}(i)
	}

	// ========== 监听系统退出信号 ==========
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	<-sigCh
	fmt.Println("\n⚠️ 收到系统退出信号!")

	// 只通知生产者停工,**不强制中断工人**
	close(stopProducing)

	// ========== 等待整个流水线排空 ==========
	fmt.Println("正在等待流水线清空所有任务...")
	wg.Wait()

	fmt.Println("✅ 所有任务处理完毕,程序体面退出!")
}

模拟中途按 Ctrl+C 的输出:

生产者:发布任务 1
中间工人 3:处理任务 1 → 产出 2
最终消费者 1:收到成品 2
...
生产者:发布任务 18
^C
⚠️ 收到系统退出信号!
生产者:收到停工指令,不再接新单!
中间工人 5:处理任务 18 → 产出 36
最终消费者 2:收到成品 36
...
中间工人 1:ch1 已关,下班!
所有中间工人下班,ch2 关闭!
最终消费者 3:无新货,收工!
✅ 所有任务处理完毕,程序体面退出!

完美! 即使中途被叫停,也保证了:

  • 不再接新任务
  • 已接任务全部完成
  • 不 panic、不泄漏 goroutine

🛡️ 八、防卡死兜底:加个"超时保险"

虽然我们希望排空,但万一某个任务卡死呢?加个 30 秒超时:

// 在 wg.Wait() 前加超时保护
done := make(chan struct{})
go func() {
	wg.Wait()
	close(done)
}()

select {
case <-done:
	fmt.Println("优雅退出成功!")
case <-time.After(30 * time.Second):
	fmt.Println("❌ 超时!强制退出(可能有数据未处理)")
	os.Exit(1)
}

📜 九、优雅退出黄金法则(背下来!)

角色 是否响应 ctx.Done() 退出方式
生产者 / 请求入口 ✅ 是 收到信号后停止接收新任务
中间处理流水线 ❌ 否 只响应 channel 关闭,排空缓冲
最终消费者 ❌ 否 for range 自动退出
主程序 ✅ 是(用于触发生产者停止) WaitGroup + 超时兜底

🎉 结语:优雅,是一种修养

在 Go 的世界里,优雅退出不是"能不能",而是"愿不愿"。

多花 10 行代码,就能避免线上事故、数据丢失、半夜告警——这波不亏!

记住:真正的优雅,不是走得快,而是走得干净。
轻轻的我走了,正如我轻轻的来,
挥挥手,不带走一片云彩!
… …

往期部分文章列表

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐