golang chan
Golang Chan Select用法
Chan
有两种chan
//无缓冲
done := make(chan bool)
//有缓冲
done := make(chan bool,1)
- 无缓冲通道
- 发送者和接收者必须同步
- 有缓冲通道
- 发送者和接收者异步(除非缓冲满了,就变成同步了)
无缓冲用法
当你需要确保两个协程 (Goroutine) 完全同步时。例如:主线程必须等待子线程结束信号。
func main() {
done := make(chan bool)
go func() {
fmt.Println("任务进行中...")
done <- true // 运行到这里会停住,直到主线程开始读 done
}()
<-done // 等待信号,确保子线程跑完了
}
有缓冲用法
场景一:防止死锁( Goroutine 泄漏)
func requestWithTimeout() {
done := make(chan bool) // 无缓冲
go func() {
// 模拟耗时操作2秒
time.Sleep(2 * time.Second)
done <- true // 如果主函数超时走了,这里会永久阻塞!这个 Goroutine 永远不会销毁。
}()
select {
case <-done:
//任务2秒
fmt.Println("任务完成")
//等待1秒,直接退出
case <-time.After(1 * time.Second):
fmt.Println("超时退出")
// 函数结束了,但上面的子协程还在等别人接收 done
}
}
解决方案(有缓冲):
done := make(chan bool, 1) // 即使没人收,子协程也能把 true 放进“盒子”里然后正常结束
场景二:解耦
在某些高并发场景下,我们希望“通知”这个动作本身是非阻塞的。发送者只需要负责“拍一下灯”,至于灯什么时候亮、谁来处理,发送者不想停下来等。
例子:系统告警通知
假设你的系统在发现错误时需要发送一个告警信号。如果告警组件反应慢,你不希望整个业务逻辑因此被拖慢
type Monitor struct {
alertChan chan bool
}
//如果现在能发出去就发,发不出去(通道满了)我就不等了,直接跳过。
func (m *Monitor) Notify() {
// 使用容量为 1 的缓冲
select {
//注意如果alertChan满了,由于select的缘故不会一直阻塞,而是选择default
case m.alertChan <- true:
// 成功放入缓冲区
default:
// 如果缓冲区满了(说明上一个告警还没处理完),就直接跳过
// 避免了业务逻辑因为告警组件卡顿而产生延迟
}
}
func main() {
m := &Monitor{
alertChan: make(chan bool, 1),
}
// 模拟业务触发告警
m.Notify()
fmt.Println("业务逻辑继续执行,未受告警阻塞")
}
select 的最佳实践
1.配合 for 循环实现“常驻监听”
select 本身只执行一次。在实际开发中,我们通常需要协程一直运行,直到收到退出信号。
for {
select {
case data := <-dataChan:
process(data)
case <-stopChan:
fmt.Println("停止运行")
return // 退出循环,结束协程
}
}
注意:不需要defalut,
2.永远要考虑“超时控制”
为了防止由于某个通道永远没数据而导致协程永久阻塞(死锁),建议在关键业务逻辑中使用 time.After。
select {
case res := <-callRPC():
fmt.Println("成功:", res)
case <-time.After(3 * time.Second):
fmt.Println("RPC 调用超时")
}
3.使用 default 实现“非阻塞操作”
如果你希望程序在通道没准备好时不等待,而是立刻去干别的事,
-
丢弃过载请求
-
一边干活,一边抽空看一眼信号是否到达
select {
case ch <- msg:
fmt.Println("发送成功")
default:
// 如果 ch 满了且无缓冲,会走到这里
fmt.Println("通道已满,消息被丢弃,避免阻塞业务")
}
###
func runTask(stopChan chan bool) {
for {
select {
case <-stopChan:
// 只要收到信号,立刻结束
fmt.Println("收到停止信号,清理资源中...")
return
default:
// 如果没有信号,就执行一段“小任务”
doHeavyWork()
// 关键:这里不需要 sleep 太久,或者根据业务频率决定
fmt.Println("正在处理一小块数据...")
}
}
}
func doHeavyWork() {
// 模拟一段耗时操作
time.Sleep(500 * time.Millisecond)
}
4.正确处理 nil 通道(动态开关)
向 nil 通道发送或接收数据会永久阻塞,但在 select 中,nil 通道的 case 会被直接忽略,利用这个特性可以动态地“关掉”某个 case。
var ch1 chan int = make(chan int)
// 某些逻辑后...
ch1 = nil // 之后 select 将再也不会进入 ch1 的分支
处理 Channel 的内存泄漏
启动很多goroutine,里面有channel,但没人写 channel,导致这些 goroutine 永远存在。
场景1:只写不读
func worker(ch chan int) {
for v := range ch {
fmt.Println(v)
}
}
场景2:只读不写
func main() {
ch := make(chan int)
go func() {
ch <- 1 // 阻塞
}()
time.Sleep(time.Hour)
}
场景3:for range channel 没关闭
func worker(ch chan int) {
for v := range ch {
fmt.Println(v)
}
}
场景4:select 没有退出机制
func worker(ch chan int) {
for {
select {
case v := <-ch:
fmt.Println(v)
}
}
}
解决
方案1 对于for range
close(ch)
方案2 对于只读不写
func worker(ctx context.Context, ch chan int) {
for {
select {
case v := <-ch:
fmt.Println(v)
//Done() 返回一个 channel
case <-ctx.Done():
return
}
}
}
main{
//创建一个context
ctx, cancel := context.WithCancel(context.Background())
//传入worker
go worker(ctx,chann)
//调用cancel,close(ctx.done),ctx.Done()产生的channel,会立即返回(不会阻塞)
cancel()
}
方案3 使用default
select {
case ch <- data:
default:
// drop
}