golang chan

Golang Chan Select用法

Chan

有两种chan

//无缓冲
done := make(chan bool)
//有缓冲
done := make(chan bool,1)
  1. 无缓冲通道
    1. 发送者和接收者必须同步
  2. 有缓冲通道
    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
}