Go 语言 select 语句

1、概述

  • select 是 Go 中的一个控制结构,类似于 switch 语句;
  • select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收;
  • select 语句会监听所有指定的通道上的操作:
    • 一旦其中一个通道准备好就会执行相应的代码块;
    • 如果多个通道都准备好,那么 select 语句会随机选择一个通道执行;
    • 如果所有通道都没有准备好,那么执行 default 块中的代码;
    • 如果所有通道都没有准备好,并且没有 default 块,那么 select 语句将会阻塞,直到至少一个通道准备好为止;(这意味着程序会一直等待,直到有一个通道可以进行操作)

2、语法

select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}
  • 每个 case 都必须是一个通道;
  • 所有 channel 表达式都会被求值;
  • 所有被发送的表达式都会被求值;
  • 如果任意摸个通道可以运行,它就执行,其他被忽略;
  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行;
    • 否则,如果有 default 子句,则执行该语句;
    • 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;
    • Go不会重新对 channel 或值进行求值;

3、简单实例

  • 没有 default 的 select;
func SimpleDemoNoDefault() {
	c1 := make(chan string)
	c2 := make(chan string)

	go func() {
		time.Sleep(1 * time.Second)
		c1 <- "channel one"
	}()

	go func() {
		time.Sleep(1 * time.Second)
		c2 <- "channel two"
	}()

	for i := 0; i < 3; i++ {
		select {
		case msg1 := <-c1:
			fmt.Println("received", msg1)
		case msg2 := <-c2:
			fmt.Println("received", msg2)
		}
	}
}
  • 有 default 的 select:
func SimpleDemoWithDefault() {
	c1 := make(chan string)
	c2 := make(chan string)

	go func() {
		for {
			c1 <- "from 1"
		}
	}()

	go func() {
		for {
			c1 <- "from 2"
		}
	}()

	for {
		time.Sleep(1 * time.Second)
		select {
		case msg1 := <-c1:
			fmt.Println(msg1)
		case msg2 := <-c2:
			fmt.Println(msg2)
		default:
			fmt.Println("no message received")
		}
	}
}

4、实现原理

通过 select 语句,可以实现主线程和其他线程之间的互动;

实现原理

详情参考:https://www.jb51.net/article/259610.htm

  • GoLang实现select时,定义了一个数据结构表示每个case语句(包含defaultdefault实际上是一种特殊的case);
  • select执行过程可以看成一个函数,函数输入case数组,输出选中的case,然后程序流程转到选中的case块;

执行流程

创建select
注册case
执行select
释放select
  • 在默认情况下,select 在编译阶段经过如下过程的处理:
    • 将所有 case 转换成包含 channel 以及类型等信息的 scase 结构体;
    • 编译器调用运行时函数 selectgo 来执行 select 语句,这个函数会根据 scase 结构体数组中的各个 case 条件,选择其中一个 case 来执行,并返回被选择 scase 结构体的索引;
    • 如果当前 scase 是一个接收操作,函数会返回一个布尔值,表示接收操作是否成功;
    • 编译器会根据 scase 结构体数组中的各个 case 条件,生成一组 if 语句,每个 if 语句会判断当前的 case 是否是被选择的 case,如果是,则执行相应的操作;

scase 数据结构

源码包 src/runtime/select.go

type scase struct {
	c    *hchan         // chan
	elem unsafe.Pointer // data element
}
  • c:表示通道,存储 case 所关联的通道,不包含 default;这也说明了一个case 语句只能操作一个 channel;
  • elem:表示数据类型,用于存储 case 所关联的数据元素;

判断某个 scase 属于什么操作

select {
case x := <-ch:
    fmt.Println("接收到数据:", x)
case ch <- 10:
    fmt.Println("发送数据成功")
default:
    fmt.Println("没有进行任何操作")
}
  • <-ch:表示接收操作,将通道 ch 中的数据赋值给变量 x;
  • ch <- 10:表示发送操作,将数据 10 发送到通道 ch 中;
  • default:表示默认操作,当没有其他 case 可执行时,执行该操作;

5、应用场景

多通道读取

func MultiChannelRead() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		ch1 <- 1
	}()

	go func() {
		ch2 <- 2
	}()

	select {
	case data := <-ch1:
		fmt.Println("received data from ch1: ", data)
	case data := <-ch2:
		fmt.Println("receiver data from ch2: ", data)
	}
}

多通道写入

func MultiChannelWrite() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		ch1 <- 1
	}()

	go func() {
		ch2 <- 2
	}()

	select {
	case <-ch1:
		fmt.Println("Send data to ch1")
	case <-ch2:
		fmt.Println("Send data to ch2")
	}
}

超时控制

func TimeoutControl() {
	ch := make(chan int)

	go func() {
		time.Sleep(2 * time.Second)
	}()

	select {
	case data := <-ch:
		fmt.Println("Received data: ", data)
	case <-time.After(1 * time.Second):
		fmt.Println("Timeout occurred")
	}
}
Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐