Go初级之九:Select 与并发控制

在Go语言中,select语句是处理并发编程的核心工具之一。它让我们能够优雅地管理多个通道操作,实现高效的并发控制。

1. Select 语句基础

1.1 Select 的基本语法

go 复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    // 启动两个goroutine发送数据
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- "来自通道1的数据"
    }()
    
    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- "来自通道2的数据"
    }()
    
    // 使用select等待任一通道有数据
    select {
    case msg1 := <-ch1:
        fmt.Println("收到:", msg1)
    case msg2 := <-ch2:
        fmt.Println("收到:", msg2)
    }
}

1.2 Select 的工作原理

  • select会同时监听所有case中的通道操作
  • 哪个通道先准备好,就执行对应的case
  • 如果多个通道同时准备好,随机选择一个执行
  • 所有case都阻塞时,select也会阻塞

2. Select 的高级用法

2.1 超时控制

go 复制代码
func withTimeout() {
    ch := make(chan string)
    
    go func() {
        // 模拟耗时操作
        time.Sleep(3 * time.Second)
        ch <- "操作完成"
    }()
    
    select {
    case result := <-ch:
        fmt.Println("成功:", result)
    case <-time.After(2 * time.Second):
        fmt.Println("操作超时")
    }
}

2.2 默认情况(default)

go 复制代码
func nonBlocking() {
    ch := make(chan string, 1)
    
    // 尝试读取,不阻塞
    select {
    case msg := <-ch:
        fmt.Println("收到:", msg)
    default:
        fmt.Println("通道为空,不等待")
    }
    
    // 尝试写入,不阻塞
    select {
    case ch <- "数据":
        fmt.Println("数据写入成功")
    default:
        fmt.Println("通道已满,写入失败")
    }
}

2.3 结合for循环

go 复制代码
func continuousProcessing() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        for i := 1; i <= 5; i++ {
            ch1 <- fmt.Sprintf("消息%d", i)
            time.Sleep(time.Second)
        }
        close(ch1)
    }()
    
    go func() {
        for i := 1; i <= 3; i++ {
            ch2 <- fmt.Sprintf("通知%d", i)
            time.Sleep(2 * time.Second)
        }
        close(ch2)
    }()
    
    // 持续监听两个通道
    for {
        select {
        case msg, ok := <-ch1:
            if !ok {
                ch1 = nil // 关闭后设为nil,不再监听
                fmt.Println("通道1已关闭")
            } else {
                fmt.Println("处理:", msg)
            }
        case notify, ok := <-ch2:
            if !ok {
                ch2 = nil
                fmt.Println("通道2已关闭")
            } else {
                fmt.Println("通知:", notify)
            }
        }
        
        // 当两个通道都关闭时退出
        if ch1 == nil && ch2 == nil {
            break
        }
    }
}

3. 并发控制模式

3.1 信号量模式

go 复制代码
func semaphore() {
    const maxWorkers = 3
    semaphore := make(chan struct{}, maxWorkers)
    results := make(chan string, 10)
    
    tasks := []string{"任务1", "任务2", "任务3", "任务4", "任务5"}
    
    // 启动工作goroutine
    for _, task := range tasks {
        go func(t string) {
            semaphore <- struct{}{} // 获取信号量
            
            // 模拟工作
            time.Sleep(time.Second)
            results <- fmt.Sprintf("完成: %s", t)
            
            <-semaphore // 释放信号量
        }(task)
    }
    
    // 收集结果
    for i := 0; i < len(tasks); i++ {
        fmt.Println(<-results)
    }
}

3.2 超时与取消

go 复制代码
func timeoutAndCancel() {
    ch := make(chan string)
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    go func() {
        // 模拟长时间操作
        time.Sleep(5 * time.Second)
        ch <- "操作完成"
    }()
    
    select {
    case result := <-ch:
        fmt.Println("结果:", result)
    case <-ctx.Done():
        fmt.Println("操作被取消:", ctx.Err())
    }
}

3.3 多路复用

go 复制代码
func multiplexing() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    ch3 := make(chan string)
    
    // 模拟不同来源的数据
    go func() {
        for i := 1; i <= 3; i++ {
            ch1 <- fmt.Sprintf("用户%d上线", i)
            time.Sleep(800 * time.Millisecond)
        }
        close(ch1)
    }()
    
    go func() {
        for i := 1; i <= 2; i++ {
            ch2 <- fmt.Sprintf("订单%d创建", i)
            time.Sleep(1200 * time.Millisecond)
        }
        close(ch2)
    }()
    
    go func() {
        for i := 1; i <= 4; i++ {
            ch3 <- fmt.Sprintf("日志%d记录", i)
            time.Sleep(500 * time.Millisecond)
        }
        close(ch3)
    }()
    
    // 统一处理所有事件
    for {
        select {
        case msg, ok := <-ch1:
            if !ok {
                ch1 = nil
            } else {
                fmt.Printf("[用户事件] %s\n", msg)
            }
        case msg, ok := <-ch2:
            if !ok {
                ch2 = nil
            } else {
                fmt.Printf("[订单事件] %s\n", msg)
            }
        case msg, ok := <-ch3:
            if !ok {
                ch3 = nil
            } else {
                fmt.Printf("[日志事件] %s\n", msg)
            }
        }
        
        if ch1 == nil && ch2 == nil && ch3 == nil {
            break
        }
    }
}

4. 实际应用示例

4.1 健康检查服务

go 复制代码
func healthCheckService() {
    checkInterval := time.Tick(5 * time.Second)
    forceCheck := make(chan struct{})
    stop := make(chan struct{})
    
    go func() {
        for {
            select {
            case <-checkInterval:
                fmt.Println("定时健康检查...")
                performHealthCheck()
            case <-forceCheck:
                fmt.Println("强制健康检查...")
                performHealthCheck()
            case <-stop:
                fmt.Println("服务停止")
                return
            }
        }
    }()
    
    // 模拟外部触发
    time.Sleep(3 * time.Second)
    forceCheck <- struct{}{}
    
    time.Sleep(8 * time.Second)
    stop <- struct{}{}
    
    time.Sleep(1 * time.Second)
}

func performHealthCheck() {
    // 模拟健康检查逻辑
    fmt.Println("  ✓ 数据库连接正常")
    fmt.Println("  ✓ 缓存服务正常")
    fmt.Println("  ✓ 网络连接正常")
}

4.2 并发下载器

go 复制代码
func concurrentDownloader() {
    urls := []string{
        "https://example.com/file1",
        "https://example.com/file2", 
        "https://example.com/file3",
        "https://example.com/file4",
    }
    
    const maxConcurrent = 2
    semaphore := make(chan struct{}, maxConcurrent)
    results := make(chan string, len(urls))
    
    for _, url := range urls {
        go func(u string) {
            semaphore <- struct{}{}
            defer func() { <-semaphore }()
            
            // 模拟下载
            duration := time.Duration(rand.Intn(3000)+1000) * time.Millisecond
            time.Sleep(duration)
            results <- fmt.Sprintf("下载完成: %s (%v)", u, duration)
        }(url)
    }
    
    // 收集结果,带超时
    timeout := time.After(5 * time.Second)
    completed := 0
    
    for completed < len(urls) {
        select {
        case result := <-results:
            fmt.Println(result)
            completed++
        case <-timeout:
            fmt.Println("下载超时")
            return
        }
    }
}

5. 最佳实践

5.1 错误处理

go 复制代码
func robustSelect() {
    ch := make(chan string)
    errCh := make(chan error)
    
    go func() {
        // 可能出错的操作
        if rand.Float32() < 0.3 {
            errCh <- fmt.Errorf("随机错误发生")
            return
        }
        
        ch <- "正常结果"
    }()
    
    select {
    case result := <-ch:
        fmt.Println("成功:", result)
    case err := <-errCh:
        fmt.Printf("错误: %v\n", err)
    case <-time.After(3 * time.Second):
        fmt.Println("操作超时")
    }
}

5.2 资源清理

go 复制代码
func cleanupExample() {
    dataCh := make(chan string)
    done := make(chan struct{})
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()
    
    go func() {
        for range ticker.C {
            select {
            case dataCh <- "新数据":
                fmt.Println("数据发送")
            case <-done:
                fmt.Println("发送器停止")
                return
            default:
                // 非阻塞发送
                fmt.Println("缓冲区满,跳过")
            }
        }
    }()
    
    // 消费数据
    time.Sleep(2 * time.Second)
    close(done) // 通知发送器停止
    
    // 继续消费剩余数据
    for {
        select {
        case data, ok := <-dataCh:
            if !ok {
                return
            }
            fmt.Printf("消费: %s\n", data)
        default:
            return // 无数据可消费
        }
    }
}

总结

select是Go并发编程的利器,主要用途包括:

  1. 多通道监听:同时处理多个通道操作
  2. 超时控制:避免无限等待
  3. 非阻塞操作 :使用default实现非阻塞
  4. 并发协调:结合信号量控制并发度
  5. 优雅退出 :配合context实现取消机制

关键要点:

  • select是随机选择就绪的case
  • default用于非阻塞操作
  • 结合time.After实现超时
  • 使用nil通道来停止监听
  • 注意资源清理和错误处理

掌握select的使用,能让你写出更健壮、更高效的并发程序。

相关推荐
不要再敲了3 小时前
Java 流程控制:从入门到面试的全方位指南
java·开发语言·面试
@HNUSTer4 小时前
基于 HTML、CSS 和 JavaScript 的智能图像虚化系统
开发语言·前端·javascript·css·html
Victor3564 小时前
Redis(44)Redis哨兵的工作原理是什么?
后端
Victor3564 小时前
Redis(45)哨兵模式与集群模式有何区别?
后端
玉木子6 小时前
机器学习(六)朴素贝叶斯分类
开发语言·人工智能·python·算法·机器学习·分类
chillxiaohan8 小时前
GO学习记录九——数据库触发器的使用+redis缓存策略
数据库·缓存·golang
Dxy12393102168 小时前
Python如何处理非标准JSON
开发语言·python·json
q567315238 小时前
从开发到部署深度解析Go与Python爬虫利弊
爬虫·python·golang