一、Select解决什么问题?
在Golang中,两个协程之间通信Channel(图一),在接受协程中通过代码表示即为<ch;如果协程需要监听多个Channel,只要有其中一个满足条件,就执行相应的逻辑(图二),这种select的应用场景之一,代码如下:
func TestSelect(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int , 1)
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
default:
}
}
上述代码,创建两个通道,通过select监听协程是否有数据,如果有打印相应的值,如果没有通过default结束程序运行;
二、Select常用用法
循环阻塞监测
func TestLoopSelect(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
go func() {
// 每隔1s发送一条消息到channel中
for range time.Tick(1 * time.Second) {
ch1 <- 1
}
}()
for {
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
}
}
}
这种写法特别常见,起一个协程,阻塞循环监听多个channel,如果有数据执行对应的操作。比如说
// go-zero/core/discov/internal/registry.go
func (c *cluster) watchStream(cli EtcdClient, key string, rev int64) bool {
var rch clientv3.WatchChan
if rev != 0 {
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix(),
clientv3.WithRev(rev+1))
} else {
rch = cli.Watch(clientv3.WithRequireLeader(c.context(cli)), makeKeyPrefix(key), clientv3.WithPrefix())
}
for {
select {
case wresp, ok := <-rch:
...
c.handleWatchEvents(key, wresp.Events)
case <-c.done:
return true
}
}
}
上述代码,通过for + select阻塞循环监测注册中心数据是否有变换,有变化的话,针对变化类型,执行对应逻辑;
非阻塞监控
func TestSelect(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int , 1)
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
default:
}
}
这段代码的意思是,程序执行到select,就检查一下channel里面是否有数据,有就处理,没有就退出;在grpc-go中,
// grpc-go/clientconn.go
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
cc := &ClientConn{
target: target,
conns: make(map[*addrConn]struct{}),
dopts: defaultDialOptions(),
czData: new(channelzData),
}
defer func() {
select {
case <-ctx.Done():
switch {
case ctx.Err() == err:
conn = nil
case err == nil || !cc.dopts.returnLastError:
conn, err = nil, ctx.Err()
default:
conn, err = nil, fmt.Errorf("%v: %v", ctx.Err(), err)
}
default:
}
}()
if cc.dopts.scChan != nil {
// Blocking wait for the initial service config.
select {
case sc, ok := <-cc.dopts.scChan:
if ok {
cc.sc = &sc
cc.safeConfigSelector.UpdateConfigSelector(&defaultConfigSelector{&sc})
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
上述代码中,有两个地方用到select,一个地方是非阻塞,检查contex是否被取消;另外一个是阻塞等待;
三、select原理
如果想了解select的实现,可以阅读runtime.selectgo代码。它主要包含三部分:
首先,检查一下是否有准备就绪的channel(多个channel就绪,随机选择一个),如果有,就执行;
其次,将当前goroutine包装成sudog,挂载到对应的channel上;
最后,如果channel中数据准备就绪,唤醒该协程继续执行第一步逻辑;
【注】源码细节,感兴趣并且有需求可以深入了解,但是不要陷入源码的怪圈中;
总结
本文主要讲述下面三部分内容:
从Go源码开发者的角度考虑,为什么需要select?
介绍了select常用的两种写法,一种是非阻塞的,一种是阻塞的,以及开源项目如何使用它们;
介绍了select的基本实现;
参考
Go语言设计与实现
gprc-go
go-zero