Select语句 #

一、Select概述 #

select语句用于处理多个channel操作,类似switch,但专用于channel。

二、基本语法 #

2.1 结构 #

go
select {
case v := <-ch1:
    // 从ch1接收
case ch2 <- v:
    // 向ch2发送
default:
    // 默认操作
}

2.2 示例 #

go
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(time.Second)
        ch1 <- "from ch1"
    }()
    
    go func() {
        time.Sleep(500 * time.Millisecond)
        ch2 <- "from ch2"
    }()
    
    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

三、随机选择 #

3.1 随机特性 #

当多个case同时就绪时,随机选择一个:

go
func main() {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    
    ch1 <- 1
    ch2 <- 2
    
    select {
    case v := <-ch1:
        fmt.Println("ch1:", v)
    case v := <-ch2:
        fmt.Println("ch2:", v)
    }
}

3.2 公平处理 #

go
func fairSelect(ch1, ch2 <-chan int) {
    for {
        select {
        case v := <-ch1:
            process(v)
        case v := <-ch2:
            process(v)
        }
    }
}

四、超时处理 #

4.1 time.After #

go
select {
case v := <-ch:
    fmt.Println(v)
case <-time.After(time.Second):
    fmt.Println("Timeout")
}

4.2 time.NewTimer #

go
timer := time.NewTimer(time.Second)
defer timer.Stop()

select {
case v := <-ch:
    fmt.Println(v)
case <-timer.C:
    fmt.Println("Timeout")
}

4.3 带超时的操作 #

go
func fetchWithTimeout(url string, timeout time.Duration) ([]byte, error) {
    ch := make(chan []byte)
    
    go func() {
        resp, _ := http.Get(url)
        defer resp.Body.Close()
        data, _ := io.ReadAll(resp.Body)
        ch <- data
    }()
    
    select {
    case data := <-ch:
        return data, nil
    case <-time.After(timeout):
        return nil, errors.New("timeout")
    }
}

五、非阻塞操作 #

5.1 default分支 #

go
select {
case v := <-ch:
    fmt.Println(v)
default:
    fmt.Println("No data")
}

5.2 非阻塞发送 #

go
select {
case ch <- v:
    fmt.Println("Sent")
default:
    fmt.Println("Channel full")
}

5.3 非阻塞接收 #

go
select {
case v := <-ch:
    fmt.Println(v)
default:
    fmt.Println("No data available")
}

六、循环select #

6.1 for-select模式 #

go
func main() {
    ch := make(chan int)
    done := make(chan struct{})
    
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(done)
    }()
    
    for {
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-done:
            return
        }
    }
}

6.2 带退出条件 #

go
func worker(ch <-chan int, cancel <-chan struct{}) {
    for {
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-cancel:
            return
        }
    }
}

七、实际应用 #

7.1 多数据源合并 #

go
func merge(ch1, ch2 <-chan int) <-chan int {
    out := make(chan int)
    
    go func() {
        defer close(out)
        for {
            select {
            case v, ok := <-ch1:
                if !ok {
                    ch1 = nil
                } else {
                    out <- v
                }
            case v, ok := <-ch2:
                if !ok {
                    ch2 = nil
                } else {
                    out <- v
                }
            }
            
            if ch1 == nil && ch2 == nil {
                return
            }
        }
    }()
    
    return out
}

7.2 超时重试 #

go
func retryWithTimeout(fn func() error, maxAttempts int, timeout time.Duration) error {
    for i := 0; i < maxAttempts; i++ {
        ch := make(chan error, 1)
        go func() {
            ch <- fn()
        }()
        
        select {
        case err := <-ch:
            if err == nil {
                return nil
            }
        case <-time.After(timeout):
            continue
        }
    }
    return errors.New("max attempts reached")
}

7.3 心跳检测 #

go
func heartbeat(interval time.Duration, done <-chan struct{}) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            fmt.Println("Heartbeat")
        case <-done:
            return
        }
    }
}

7.4 限流 #

go
func rateLimiter(requests <-chan Request, limit int) {
    ticker := time.NewTicker(time.Second / time.Duration(limit))
    defer ticker.Stop()
    
    for req := range requests {
        <-ticker.C
        go process(req)
    }
}

八、常见模式 #

8.1 优雅退出 #

go
func server(shutdown <-chan struct{}) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            // 处理任务
        case <-shutdown:
            // 清理资源
            return
        }
    }
}

8.2 多任务等待 #

go
func waitAll(tasks []Task) {
    done := make(chan struct{}, len(tasks))
    
    for _, task := range tasks {
        go func(t Task) {
            t.Run()
            done <- struct{}{}
        }(task)
    }
    
    for i := 0; i < len(tasks); i++ {
        <-done
    }
}

8.3 取消操作 #

go
func withCancel(ctx context.Context, fn func() error) error {
    ch := make(chan error, 1)
    
    go func() {
        ch <- fn()
    }()
    
    select {
    case err := <-ch:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}

九、常见错误 #

9.1 忘记default #

go
select {
case v := <-ch:
    fmt.Println(v)
// 忘记default会阻塞
}

9.2 goroutine泄漏 #

go
func leak() {
    ch := make(chan int)
    go func() {
        select {
        case v := <-ch:
            fmt.Println(v)
        }
    }()
    // 如果ch永不发送,goroutine泄漏
}

9.3 饥饿问题 #

go
for {
    select {
    case v := <-ch1:
        // 如果ch1持续有数据,ch2可能饥饿
    case v := <-ch2:
    }
}

十、最佳实践 #

10.1 使用context #

go
select {
case v := <-ch:
    fmt.Println(v)
case <-ctx.Done():
    return ctx.Err()
}

10.2 避免空select #

go
select {}  // 永久阻塞

10.3 处理nil channel #

go
var ch1 chan int  // nil

select {
case v := <-ch1:  // 永不选中
    fmt.Println(v)
case v := <-ch2:
    fmt.Println(v)
}

十一、总结 #

Select要点:

特性 说明
随机选择 多个就绪时随机选一个
阻塞 无就绪case时阻塞
default 无就绪case时执行default
超时 配合time.After使用

关键点:

  1. 多路复用:同时监听多个channel
  2. 随机选择:多个就绪时随机选择
  3. 超时处理:配合time.After实现超时
  4. 非阻塞:使用default实现非阻塞
  5. 循环模式:for-select处理持续事件

准备好学习Channel模式了吗?让我们进入下一章!

最后更新:2026-03-26