ZetCode

Golang select 关键字

最后修改于 2025 年 5 月 7 日

本教程将解释如何在 Go 中使用 select 关键字。我们将通过并发模式的实际示例来介绍通道操作基础知识。

select 语句允许一个 goroutine 等待多个通信操作。如果多个 case 都准备就绪,它会随机选择一个。

在 Go 中,select 对于 goroutine 之间的协调至关重要。它处理通道的发送/接收,并实现超时和非阻塞操作。

带两个通道的基本 select

此示例展示了 select 与两个通道的最简单用法。它等待来自任一通道的数据。

basic_select.go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from ch2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

select 会等待任一通道有数据。由于 ch1 更快,其消息会先打印。循环确保两个消息都能接收到。

带 default case 的 select

添加 default case 使 select 变为非阻塞。此示例演示了在没有通道就绪时立即执行。

default_select.go
package main

import "fmt"

func main() {
    ch := make(chan string)

    select {
    case msg := <-ch:
        fmt.Println("Received:", msg)
    default:
        fmt.Println("No message received")
    }
}

由于通道为空且没有 goroutine 发送,default case 会立即执行。这种模式对于轮询通道很有用。

带超时的 select

在 select 中使用 time.After 来实现超时。这可以防止通道操作无限期阻塞。

timeout_select.go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "result"
    }()

    select {
    case res := <-ch:
        fmt.Println(res)
    case <-time.After(2 * time.Second):
        fmt.Println("timeout")
    }
}

超时 case 在 2 秒后触发,而 goroutine 需要 3 秒。这确保了程序不会无限期地等待慢速操作。

用于发送和接收的 select

Select 可以处理发送和接收操作。此示例演示了不同通道方向之间的协调。

send_receive_select.go
package main

import (
    "fmt"
    "time"
)

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

    go func() {
        for {
            select {
            case ch <- 1:
                fmt.Println("Sent 1")
            case <-done:
                fmt.Println("Stopping")
                return
            }
        }
    }()

    go func() {
        time.Sleep(2 * time.Second)
        done <- true
    }()

    for i := 0; i < 5; i++ {
        fmt.Println("Received:", <-ch)
        time.Sleep(300 * time.Millisecond)
    }
}

第一个 goroutine 在发送值和检查 done 通道之间交替。2 秒后,done 通道会停止发送者。

带多个就绪 case 的 select

当多个 case 就绪时,select 会随机选择一个。此示例展示了非确定性行为。

random_select.go
package main

import "fmt"

func main() {
    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)

    ch1 <- "one"
    ch2 <- "two"

    for i := 0; i < 10; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
            ch1 <- "one"
        case msg := <-ch2:
            fmt.Println(msg)
            ch2 <- "two"
        }
    }
}

两个通道都已就绪,因此 select 会在它们之间随机选择。输出显示了“one”和“two”的混合,顺序不可预测。

带 nil 通道的 select

Select 会忽略 nil 通道。此示例演示了如何使用 nil 来动态禁用 case。

nil_select.go
package main

import (
    "fmt"
    "time"
)

func main() {
    var ch chan string

    go func() {
        time.Sleep(2 * time.Second)
        ch = make(chan string, 1)
        ch <- "message"
    }()

    for {
        select {
        case msg := <-ch:
            fmt.Println("Received:", msg)
            return
        default:
            fmt.Println("No channel yet")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

最初 ch 是 nil,因此只有 default case 会执行。2 秒后,ch 变为活动状态,可以接收消息。

实际示例:Worker Pool

这个实际示例展示了如何在 worker pool 模式中使用 select 来协调多个 goroutine。

worker_select.go
package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d started job %d\n", id, j)
        time.Sleep(time.Second)
        fmt.Printf("worker %d finished job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    for a := 1; a <= 5; a++ {
        select {
        case res := <-results:
            fmt.Println("result:", res)
        case <-time.After(2 * time.Second):
            fmt.Println("timeout waiting for result")
        }
    }
}

select 会等待 worker 的结果,并带有超时。它确保程序不会在 worker 未能完成其任务时挂起。

来源

Go 语言规范

本教程通过并发编程中通道操作的实际示例,讲解了 Go 中的 select 关键字。

作者

我叫 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 Golang 教程