Go 语言中 select的使用 | 豆包MarsCode AI刷题

112 阅读3分钟

Go 语言中 select 的用法

谈到select大家可能会想到swicth,相信大家对 switch 并不陌生,selectswitch 的共同点在于都通过 case 来处理,但两者处理的内容完全不同,且互不兼容。


switch 的特性

switch 语句可以处理各种类型和类型断言操作,例如 interface{} 类型判断。以下是 switch 的几个特性:

  • 可用于各种数据类型的值判断。
  • 支持 interface{}.(type) 类型断言。
  • 按照 case 顺序逐一匹配并执行。

例如

package main

var (
    i interface{}
)

func convert(i interface{}) {
    switch t := i.(type) {
    case int:
        println("i is integer", t)
    case string:
        println("i is string", t)
    case float64:
        println("i is float64", t)
    default:
        println("type not found")
    }
}

func main() {
    i = 100
    convert(i)
    i = float64(45.55)
    convert(i)
    i = "foo"
    convert(i)
    convert(float32(10.0))
}

结果

i is integer 100
i is float64 +4.555000e+001
i is string foo
type not found

select 的特性

在go语言中select 专用于处理 channel,如果不是 channel,会报错。以下是 select 的几个关键点:

  • default 分支会直接执行。
  • 如果没有 defaultselect 会阻塞。
  • 如果未向 channel 发送数据,可能会引发死锁(panic)。

以下是一些 select 的实际用法和示例:


随机选择 Random Select

当多个 case 中涉及同一个 channel 时,select 会随机选择一个分支执行。

package main

import "fmt"

func main() {
    ch := make(chan int, 1)

    ch <- 1
    select {
    case <-ch:
        fmt.Println("random 01")
    case <-ch:
        fmt.Println("random 02")
    }
}
结果:

执行时有时输出 random 01,有时输出 random 02。这是因为 select 会在有多个匹配 case 时随机选择执行。

如果没有向 channel 发送数据,程序会因为死锁(deadlock)而崩溃。例如:

func main() {
    ch := make(chan int, 1)

    select {
    case <-ch:
        fmt.Println("random 01")
    case <-ch:
        fmt.Println("random 02")
    }
}
如何解决

通过增加 default 分支可以避免死锁:

func main() {
    ch := make(chan int, 1)

    select {
    case <-ch:
        fmt.Println("random 01")
    case <-ch:
        fmt.Println("random 02")
    default:
        fmt.Println("exit")
    }
}

运行后会输出 exit,主程序不会因为没有值读取而阻塞。


超时机制 Timeout

使用 select 读取 channel 时,通常会实现一个超时机制,避免长时间阻塞。例如:

package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make(chan bool, 1)
    go func() {
        time.Sleep(2 * time.Second)
        timeout <- true
    }()
    ch := make(chan int)
    select {
    case <-ch:
    case <-timeout:
        fmt.Println("timeout 01")
    }
}

通过向 timeout channel 发送值来触发超时。也可以使用 time.After 来实现超时:

select {
case <-ch:
case <-timeout:
    fmt.Println("timeout 01")
case <-time.After(time.Second * 1):
    fmt.Println("timeout 02")
}

time.After 会返回一个 chan time.Time,如果超过指定时间,select 会执行相应分支。


检查 channel 是否已满

通过 selectdefault 结合,可以检查 channel 是否已满:

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 1)
    ch <- 1
    select {
    case ch <- 2:
        fmt.Println("channel value is", <-ch)
        fmt.Println("channel value is", <-ch)
    default:
        fmt.Println("channel blocking")
    }
}

运行后会输出 channel blocking,因为 ch 的缓冲区大小为 1,已经满了。

将缓冲区改为 2 后,可以继续向 channel 发送值:

func main() {
    ch := make(chan int, 2)
    ch <- 1
    select {
    case ch <- 2:
        fmt.Println("channel value is", <-ch)
        fmt.Println("channel value is", <-ch)
    default:
        fmt.Println("channel blocking")
    }
}

selectfor 的结合

当需要持续监听多个 channel 时,可以结合 forselect 实现。以下是示例代码:

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {
    LOOP:
        for {
            time.Sleep(1 * time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                break LOOP
            default:
            }
        }
    }()

    time.Sleep(time.Second * 4)
    ch <- "stop"
}
结果
1574474619
1574474620
1574474621
1574474622

程序会每秒打印当前时间戳,直到主线程向 ch 发送 "stop"


需要注意

  • 没有 default 的情况下select 会阻塞直到某个 channel 有数据。

  • defaultselect,如果没有满足的 case,default 会立即执行。

  • for + select 的场景下,如果需要跳出循环,可以使用带标签的 break,比如:

    LOOP:
        for {
            select {
            case m := <-ch:
                println(m)
                break LOOP
            }
        }